Skip to main content

web_net/
cx.rs

1//! The handler's context: the cleared request, its identity, its route
2//! parameters, and the reply being written.
3
4use std::borrow::Cow;
5use std::fmt;
6use std::str::FromStr;
7
8use serve_net::{Method, Verdict};
9
10use crate::app::Shared;
11use crate::error::Error;
12use crate::router::Captures;
13use crate::{NoRender, Output, Render, Request, RequestId, Result, Stage, TemplateId, View, url};
14
15/// The reply a handler writes. Held by web-net until the handler returns, so
16/// an error or a panic replaces it whole: nothing a failed handler set (a
17/// cookie, a header) leaves.
18#[derive(Default)]
19pub(crate) struct Out {
20    pub(crate) status: u16,
21    pub(crate) names: Vec<u8>,
22    pub(crate) values: Vec<u8>,
23    pub(crate) spans: Vec<(u32, u32, u32, u32)>,
24    pub(crate) body: Vec<u8>,
25    pub(crate) invalid: bool,
26    pub(crate) has_type: bool,
27}
28
29/// Fields a handler may not set: framing is serve-net's.
30const FRAMING: [&str; 5] = [
31    "content-length",
32    "transfer-encoding",
33    "connection",
34    "keep-alive",
35    "upgrade",
36];
37
38impl Out {
39    pub(crate) fn reset(&mut self) {
40        self.status = 200;
41        self.names.clear();
42        self.values.clear();
43        self.spans.clear();
44        self.body.clear();
45        self.invalid = false;
46        self.has_type = false;
47    }
48
49    pub(crate) fn wipe(&mut self) {
50        // black_box keeps the zeroing from being dropped as a dead store.
51        for v in [&mut self.names, &mut self.values, &mut self.body] {
52            v.iter_mut().for_each(|b| *b = 0);
53            std::hint::black_box(&v);
54        }
55    }
56
57    pub(crate) fn header(&mut self, name: &str, value: &[u8]) {
58        let ok_name = !name.is_empty()
59            && name.bytes().all(is_tchar)
60            && !FRAMING.iter().any(|f| f.eq_ignore_ascii_case(name));
61        let ok_value = value
62            .iter()
63            .all(|&b| b == b'\t' || (b >= 0x20 && b != 0x7f));
64        if !ok_name || !ok_value || self.spans.len() >= 64 {
65            self.invalid = true;
66            return;
67        }
68        if name.eq_ignore_ascii_case("content-type") {
69            self.has_type = true;
70        }
71        let (ns, vs) = (self.names.len() as u32, self.values.len() as u32);
72        self.names
73            .extend(name.bytes().map(|b| b.to_ascii_lowercase()));
74        self.values.extend_from_slice(value);
75        self.spans
76            .push((ns, self.names.len() as u32, vs, self.values.len() as u32));
77    }
78
79    pub(crate) fn get(&self, name: &str) -> Option<&[u8]> {
80        self.spans
81            .iter()
82            .find(|&&(s, e, _, _)| &self.names[s as usize..e as usize] == name.as_bytes())
83            .map(|&(_, _, vs, ve)| &self.values[vs as usize..ve as usize])
84    }
85
86    /// Drops every field called `name`.
87    pub(crate) fn remove(&mut self, name: &str) {
88        let names = &self.names;
89        self.spans
90            .retain(|&(s, e, _, _)| &names[s as usize..e as usize] != name.as_bytes());
91    }
92
93    /// Whether a `Set-Cookie` field sets the cookie `cookie` (by name).
94    pub(crate) fn sets_cookie(&self, cookie: &str) -> bool {
95        let name = cookie.split('=').next().unwrap_or("").trim();
96        self.spans.iter().any(|&(s, e, vs, ve)| {
97            &self.names[s as usize..e as usize] == b"set-cookie"
98                && self.values[vs as usize..ve as usize]
99                    .split(|&b| b == b'=')
100                    .next()
101                    .is_some_and(|n| n.trim_ascii() == name.as_bytes())
102        })
103    }
104
105    pub(crate) fn has(&self, name: &str) -> bool {
106        self.spans
107            .iter()
108            .any(|&(s, e, _, _)| &self.names[s as usize..e as usize] == name.as_bytes())
109    }
110}
111
112const fn is_tchar(b: u8) -> bool {
113    matches!(b,
114        b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9'
115        | b'!' | b'#' | b'$' | b'%' | b'&' | b'\'' | b'*' | b'+' | b'-' | b'.'
116        | b'^' | b'_' | b'`' | b'|' | b'~')
117}
118
119/// `path` inside a site installed under `base` (`""` or `/blog`).
120pub(crate) fn site_url(base: &str, path: &str) -> String {
121    if path == "/" && !base.is_empty() {
122        format!("{base}/")
123    } else {
124        format!("{base}{path}")
125    }
126}
127
128/// A site-relative path that is safe as a redirect target: it starts with
129/// one `/` (never `//` or `/\`, which browsers read as another host) and
130/// holds only visible ASCII.
131pub(crate) fn is_local_target(p: &str) -> bool {
132    let b = p.as_bytes();
133    b.first() == Some(&b'/')
134        && !matches!(b.get(1), Some(b'/' | b'\\'))
135        && b.iter().all(|&c| (0x21..0x7f).contains(&c) && c != b'\\')
136}
137
138/// The context a handler receives: proof that the request passed its
139/// route's access check, the identity it carries, and the reply.
140///
141/// There is no public way to make one: web-net builds it only after the
142/// gate cleared the request.
143///
144/// ```
145/// use web_net::{App, Context, Guest, get};
146///
147/// fn home(cx: &mut Context<'_, Guest>) -> web_net::Result {
148///     cx.text(200, "home")
149/// }
150///
151/// let app = App::builder().route(get("/", home)).build()?;
152/// let r = serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n");
153/// assert_eq!(r.text(), "home");
154/// # Ok::<(), web_net::BuildError>(())
155/// ```
156pub struct Context<'r, I, E: Render = NoRender> {
157    pub(crate) req: Request<'r>,
158    pub(crate) who: &'r I,
159    pub(crate) caps: Captures,
160    pub(crate) names: &'r [Box<str>],
161    pub(crate) out: &'r mut Out,
162    pub(crate) shared: &'r Shared<E>,
163    pub(crate) template: Option<TemplateId>,
164    pub(crate) binding: Option<&'r [u8]>,
165    pub(crate) limits: &'r form_net::Limits,
166    pub(crate) before_render: &'r [crate::app::HookFn<I, E>],
167    pub(crate) rendering: bool,
168}
169
170impl<'r, I, E: Render> Context<'r, I, E> {
171    // ---------------------------------------------------------------- request
172
173    /// The request.
174    ///
175    /// ```
176    /// use web_net::{App, get};
177    ///
178    /// let app = App::builder()
179    ///     .route(get("/", |cx| {
180    ///         let host = cx.request().host();
181    ///         cx.text(200, host)
182    ///     }))
183    ///     .build()?;
184    /// let r = serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n");
185    /// assert_eq!(r.text(), "localhost");
186    /// # Ok::<(), web_net::BuildError>(())
187    /// ```
188    #[must_use]
189    pub fn request(&self) -> &Request<'r> {
190        &self.req
191    }
192
193    /// The method.
194    ///
195    /// ```
196    /// use serve_net::Method;
197    /// use web_net::{App, Context, Guest, Route};
198    ///
199    /// fn which(cx: &mut Context<'_, Guest>) -> web_net::Result {
200    ///     let m = if cx.method() == Method::Post { "post" } else { "get" };
201    ///     cx.text(200, m)
202    /// }
203    ///
204    /// let app = App::builder()
205    ///     .route(Route::at("/").get(which).post(which).csrf_exempt("example"))
206    ///     .build()?;
207    /// let r = serve_net::testing::respond(&app, b"POST / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 0\r\n\r\n");
208    /// assert_eq!(r.text(), "post");
209    /// # Ok::<(), web_net::BuildError>(())
210    /// ```
211    #[must_use]
212    pub fn method(&self) -> Method {
213        self.req.method()
214    }
215
216    /// The canonical path inside the site.
217    ///
218    /// ```
219    /// use web_net::{App, get};
220    ///
221    /// let app = App::builder()
222    ///     .base("/app")
223    ///     .route(get("/about", |cx| cx.text(200, cx.path())))
224    ///     .build()?;
225    /// let r = serve_net::testing::respond(&app, b"GET /app/about HTTP/1.1\r\nHost: localhost\r\n\r\n");
226    /// assert_eq!(r.text(), "/about");
227    /// # Ok::<(), web_net::BuildError>(())
228    /// ```
229    #[must_use]
230    pub fn path(&self) -> &'r str {
231        self.req.path
232    }
233
234    /// The first value of a request field.
235    ///
236    /// ```
237    /// use web_net::{App, get};
238    ///
239    /// let app = App::builder()
240    ///     .route(get("/", |cx| {
241    ///         let agent = cx.header("user-agent").unwrap_or(b"unknown");
242    ///         cx.bytes(200, "text/plain", agent)
243    ///     }))
244    ///     .build()?;
245    /// let r = serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: localhost\r\nUser-Agent: demo/1\r\n\r\n");
246    /// assert_eq!(r.text(), "demo/1");
247    /// # Ok::<(), web_net::BuildError>(())
248    /// ```
249    #[must_use]
250    pub fn header(&self, name: &str) -> Option<&'r [u8]> {
251        self.req.header(name)
252    }
253
254    /// Who the request comes from: always present, possibly a guest.
255    ///
256    /// ```
257    /// use web_net::{App, Context, Guest, Identity, get};
258    ///
259    /// let app = App::builder()
260    ///     .route(get("/", |cx: &mut Context<'_, Guest>| {
261    ///         let who = if cx.identity().is_guest() { "guest" } else { "member" };
262    ///         cx.text(200, who)
263    ///     }))
264    ///     .build()?;
265    /// let r = serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n");
266    /// assert_eq!(r.text(), "guest");
267    /// # Ok::<(), web_net::BuildError>(())
268    /// ```
269    #[must_use]
270    pub fn identity(&self) -> &'r I {
271        self.who
272    }
273
274    /// The request's reference, as shown on error pages.
275    ///
276    /// ```
277    /// use web_net::{App, get};
278    ///
279    /// let app = App::builder()
280    ///     .route(get("/", |cx| {
281    ///         let reference = cx.id().to_string();
282    ///         cx.text(200, &reference)
283    ///     }))
284    ///     .build()?;
285    /// let r = serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n");
286    /// assert_eq!(r.text().len(), 16);
287    /// # Ok::<(), web_net::BuildError>(())
288    /// ```
289    #[must_use]
290    pub fn id(&self) -> RequestId {
291        self.req.id()
292    }
293
294    /// A route parameter (`:slug` or `*rest`), percent-decoded.
295    ///
296    /// The path is canonical (no dot segments), but a `*rest` value is part
297    /// of a URL, not a safe file path: it can hold a drive (`C:/Windows`)
298    /// or names a file system reads specially. Serve files with
299    /// [`Assets`](crate::Assets), which refuses those; never join a
300    /// parameter onto a folder.
301    ///
302    /// ```
303    /// use web_net::{App, get};
304    ///
305    /// let app = App::builder()
306    ///     .route(get("/posts/:slug", |cx| cx.text(200, &cx.param("slug")?)))
307    ///     .build()?;
308    /// let r = serve_net::testing::respond(&app, b"GET /posts/hello%20world HTTP/1.1\r\nHost: localhost\r\n\r\n");
309    /// assert_eq!(r.text(), "hello world");
310    /// # Ok::<(), web_net::BuildError>(())
311    /// ```
312    ///
313    /// # Errors
314    ///
315    /// 500 `route.no_param` when the route's pattern has no parameter
316    /// `name` (a mistake in the handler).
317    pub fn param(&self, name: &str) -> Result<Cow<'r, str>> {
318        let i = self
319            .names
320            .iter()
321            .position(|n| n.as_ref() == name)
322            .ok_or_else(|| Error::internal("route.no_param"))?;
323        let (s, e) = self
324            .caps
325            .get(i)
326            .ok_or_else(|| Error::internal("route.no_param"))?;
327        Ok(url::decode(&self.req.path[s..e]))
328    }
329
330    /// A route parameter parsed as `T`. A value that does not parse means
331    /// the page does not exist (`/users/abc` for a numeric id): 404.
332    ///
333    /// ```
334    /// use web_net::{App, get};
335    ///
336    /// let app = App::builder()
337    ///     .route(get("/users/:id", |cx| {
338    ///         let id: u64 = cx.param_as("id")?;
339    ///         cx.text(200, &format!("user {id}"))
340    ///     }))
341    ///     .build()?;
342    /// let r = serve_net::testing::respond(&app, b"GET /users/abc HTTP/1.1\r\nHost: localhost\r\n\r\n");
343    /// assert_eq!(r.status, 404);
344    /// # Ok::<(), web_net::BuildError>(())
345    /// ```
346    ///
347    /// # Errors
348    ///
349    /// 404 `route.param` when the value does not parse; as
350    /// [`param`](Self::param) for a name the pattern does not have.
351    pub fn param_as<T: FromStr>(&self, name: &str) -> Result<T> {
352        self.param(name)?
353            .parse()
354            .map_err(|_| Error::not_found("route.param"))
355    }
356
357    /// The first value of a query parameter, decoded (`+` is a space).
358    ///
359    /// ```
360    /// use web_net::{App, get};
361    ///
362    /// let app = App::builder()
363    ///     .route(get("/search", |cx| {
364    ///         let q = cx.query("q")?.unwrap_or_default();
365    ///         cx.text(200, &q)
366    ///     }))
367    ///     .build()?;
368    /// let at = |target: &str| serve_net::testing::respond(&app, format!("GET {target} HTTP/1.1\r\nHost: localhost\r\n\r\n").as_bytes());
369    /// assert_eq!(at("/search?q=red+shoes").text(), "red shoes");
370    /// assert_eq!(at("/search?q=a&q=b").status, 400);
371    /// # Ok::<(), web_net::BuildError>(())
372    /// ```
373    ///
374    /// # Errors
375    ///
376    /// 400 `query.duplicate` when the name appears twice (parameter
377    /// pollution is refused, never resolved by picking one), 400
378    /// `query.invalid` when the value does not decode to UTF-8.
379    pub fn query(&self, name: &str) -> Result<Option<Cow<'r, str>>> {
380        let Some(q) = self.req.query() else {
381            return Ok(None);
382        };
383        let mut found = None;
384        for pair in q.split('&') {
385            let (k, v) = pair.split_once('=').unwrap_or((pair, ""));
386            if decode_query(k).as_deref() != Some(name) {
387                continue;
388            }
389            if found.is_some() {
390                return Err(Error::bad_request("query.duplicate"));
391            }
392            found = Some(decode_query(v).ok_or_else(|| Error::bad_request("query.invalid"))?);
393        }
394        Ok(found)
395    }
396
397    /// The raw request body. On an unsafe method its CSRF token was
398    /// checked before the handler ran.
399    ///
400    /// ```
401    /// use web_net::{App, post};
402    ///
403    /// let app = App::builder()
404    ///     .route(post("/echo", |cx| {
405    ///         let body = cx.body()?;
406    ///         cx.bytes(200, "text/plain", body)
407    ///     }).csrf_exempt("example: echoes the body"))
408    ///     .build()?;
409    /// let r = serve_net::testing::respond(&app, b"POST /echo HTTP/1.1\r\nHost: localhost\r\nContent-Length: 5\r\n\r\nhello");
410    /// assert_eq!(r.text(), "hello");
411    /// # Ok::<(), web_net::BuildError>(())
412    /// ```
413    ///
414    /// # Errors
415    ///
416    /// None today; reading the body stays fallible so a content inspector
417    /// can refuse it in a later version without changing handlers.
418    pub fn body(&self) -> Result<&'r [u8]> {
419        Ok(self.req.http.body_bytes())
420    }
421
422    /// Reads the body as a typed form (urlencoded or multipart text fields),
423    /// with the route's [`form_limits`](crate::Route::form_limits).
424    ///
425    /// On an unsafe method the CSRF token was checked before the handler
426    /// ran. A form type that is posted with a token lists the token field in
427    /// its `ignore` list (`ignore = ["csrf_token"]`).
428    ///
429    /// The error keeps form-net's detail, so an action can render the form
430    /// again with 422; `?` turns it into an [`Error`] with its status.
431    ///
432    /// ```
433    /// use web_net::form_net::{self, FromForm};
434    ///
435    /// form_net::form! {
436    ///     pub struct Signup { pub email: String }
437    ///     ignore = ["csrf_token"];
438    /// }
439    ///
440    /// let app = web_net::App::builder()
441    ///     .route(web_net::post("/signup", |cx| {
442    ///         let form: Signup = cx.form()?;
443    ///         cx.text(200, &form.email)
444    ///     }).csrf_exempt("example without a session"))
445    ///     .build()?;
446    /// let r = serve_net::testing::respond(&app,
447    ///     b"POST /signup HTTP/1.1\r\nHost: localhost\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: 15\r\n\r\nemail=a%40b.com");
448    /// assert_eq!(r.text(), "a@b.com");
449    /// # Ok::<(), web_net::BuildError>(())
450    /// ```
451    ///
452    /// # Errors
453    ///
454    /// form-net's [`FormError`](form_net::FormError): 415 for another media
455    /// type, 413 for a budget passed, 400 for malformed input, 422 for
456    /// fields that do not fit `T`, 403 for a CSRF token.
457    pub fn form<T: form_net::FromForm>(&mut self) -> std::result::Result<T, form_net::FormError> {
458        self.form_inspected(&mut form_net::NoInspector)
459    }
460
461    /// As [`form`](Self::form), with an inspector beside the parse (a
462    /// content policy that sees every part and field first).
463    ///
464    /// ```
465    /// use web_net::form_net::{self, FromForm, Inspector, Refusal, Verdict};
466    ///
467    /// form_net::form! {
468    ///     pub struct Signup { pub email: String }
469    ///     ignore = ["csrf_token"];
470    /// }
471    ///
472    /// /// Refuses addresses at one domain.
473    /// struct NoSpam;
474    /// impl Inspector for NoSpam {
475    ///     fn field(&mut self, name: &str, value: &[u8]) -> Verdict {
476    ///         if name == "email" && value.ends_with(b"@spam.example") {
477    ///             return Verdict::Refuse(Refusal::new(422, "signup.spam"));
478    ///         }
479    ///         Verdict::Continue
480    ///     }
481    /// }
482    ///
483    /// let app = web_net::App::builder()
484    ///     .route(web_net::post("/signup", |cx| {
485    ///         let form: Signup = cx.form_inspected(&mut NoSpam)?;
486    ///         cx.text(200, &form.email)
487    ///     }).csrf_exempt("example without a session"))
488    ///     .build()?;
489    /// let r = serve_net::testing::respond(&app,
490    ///     b"POST /signup HTTP/1.1\r\nHost: localhost\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: 22\r\n\r\nemail=a%40spam.example");
491    /// assert_eq!(r.status, 422);
492    /// # Ok::<(), web_net::BuildError>(())
493    /// ```
494    ///
495    /// # Errors
496    ///
497    /// As [`form`](Self::form), plus the inspector's refusals.
498    pub fn form_inspected<T: form_net::FromForm, N: form_net::Inspector>(
499        &mut self,
500        inspector: &mut N,
501    ) -> std::result::Result<T, form_net::FormError> {
502        let content_type = self.req.header("content-type").unwrap_or(b"");
503        let body = self.req.http.body_bytes();
504        T::from_body_inspected(
505            content_type,
506            body,
507            self.limits,
508            &form_net::Policy::DEFAULT,
509            inspector,
510        )
511    }
512
513    /// Reads a multipart upload into `store` (feature `uploads`): text
514    /// fields in memory, files in the store's private temporary files, with
515    /// the route's form limits. The CSRF token was checked before the
516    /// handler ran, so a forged one stores nothing. Turn the result into a typed form with
517    /// `T::from_upload(upload)`.
518    ///
519    /// ```
520    /// use web_net::form_net::Store;
521    ///
522    /// let store = Store::memory_only();
523    /// let app = web_net::App::builder()
524    ///     .route(web_net::post("/upload", move |cx| {
525    ///         let upload = cx.upload(&store)?;
526    ///         let stored = upload.file("photo").is_some();
527    ///         cx.text(200, if stored { "stored" } else { "none" })
528    ///     }).csrf_exempt("example without a session"))
529    ///     .build()?;
530    /// let body = "--XyZ\r\nContent-Disposition: form-data; name=\"photo\"; filename=\"a.txt\"\r\nContent-Type: text/plain\r\n\r\nabc\r\n--XyZ--\r\n";
531    /// let req = format!("POST /upload HTTP/1.1\r\nHost: localhost\r\nContent-Type: multipart/form-data; boundary=XyZ\r\nContent-Length: {}\r\n\r\n{body}", body.len());
532    /// let r = serve_net::testing::respond(&app, req.as_bytes());
533    /// assert_eq!(r.text(), "stored");
534    /// # Ok::<(), web_net::BuildError>(())
535    /// ```
536    ///
537    /// # Errors
538    ///
539    /// As [`form`](Self::form), plus the store's errors.
540    #[cfg(feature = "uploads")]
541    #[cfg_attr(docsrs, doc(cfg(feature = "uploads")))]
542    pub fn upload(
543        &mut self,
544        store: &form_net::Store,
545    ) -> std::result::Result<form_net::Upload, form_net::FormError> {
546        self.upload_inspected(store, &mut form_net::NoInspector)
547    }
548
549    /// As [`upload`](Self::upload), with an inspector beside the parse.
550    ///
551    /// ```
552    /// use web_net::form_net::{Inspector, PartHead, Refusal, Store, Verdict};
553    ///
554    /// /// Takes text fields only.
555    /// struct NoFiles;
556    /// impl Inspector for NoFiles {
557    ///     fn part_start(&mut self, head: &PartHead<'_>) -> Verdict {
558    ///         if head.is_file() {
559    ///             return Verdict::Refuse(Refusal::new(422, "upload.no_files"));
560    ///         }
561    ///         Verdict::Continue
562    ///     }
563    /// }
564    ///
565    /// let store = Store::memory_only();
566    /// let app = web_net::App::builder()
567    ///     .route(web_net::post("/upload", move |cx| {
568    ///         let _upload = cx.upload_inspected(&store, &mut NoFiles)?;
569    ///         cx.text(200, "ok")
570    ///     }).csrf_exempt("example without a session"))
571    ///     .build()?;
572    /// let body = "--XyZ\r\nContent-Disposition: form-data; name=\"photo\"; filename=\"a.txt\"\r\n\r\nabc\r\n--XyZ--\r\n";
573    /// let req = format!("POST /upload HTTP/1.1\r\nHost: localhost\r\nContent-Type: multipart/form-data; boundary=XyZ\r\nContent-Length: {}\r\n\r\n{body}", body.len());
574    /// let r = serve_net::testing::respond(&app, req.as_bytes());
575    /// assert_eq!(r.status, 422);
576    /// # Ok::<(), web_net::BuildError>(())
577    /// ```
578    ///
579    /// # Errors
580    ///
581    /// As [`upload`](Self::upload), plus the inspector's refusals.
582    #[cfg(feature = "uploads")]
583    #[cfg_attr(docsrs, doc(cfg(feature = "uploads")))]
584    pub fn upload_inspected<N: form_net::Inspector>(
585        &mut self,
586        store: &form_net::Store,
587        inspector: &mut N,
588    ) -> std::result::Result<form_net::Upload, form_net::FormError> {
589        let content_type = self.req.header("content-type").unwrap_or(b"");
590        let body = self.req.http.body_bytes();
591        let mut session = form_net::UploadSession::new(
592            content_type,
593            self.limits,
594            &form_net::Policy::DEFAULT,
595            store,
596        )?;
597        session.push(body, inspector)?;
598        session.finish(inspector)
599    }
600
601    /// A CSRF token for the visitor's session, to put in a form's hidden
602    /// `csrf_token` field or a script's `X-CSRF-Token` header. Different on
603    /// every call (masked), so a compression side channel cannot recover it.
604    ///
605    /// ```
606    /// use web_net::{App, Context, Denial, Gate, Identity, Request, csrf_sec::Csrf, get};
607    ///
608    /// struct Visitor;
609    /// impl Identity for Visitor {
610    ///     fn is_guest(&self) -> bool { false }
611    ///     fn csrf_binding(&self) -> Option<&[u8]> { Some(b"the session id") }
612    /// }
613    /// # struct Sessions;
614    /// # impl Gate for Sessions {
615    /// #     type Identity = Visitor;
616    /// #     type Rule = ();
617    /// #     fn authenticate(&self, _: &Request<'_>) -> Result<Visitor, Denial> { Ok(Visitor) }
618    /// #     fn authorize(&self, _: &Visitor, _: &(), _: &Request<'_>) -> Result<(), Denial> { Ok(()) }
619    /// # }
620    ///
621    /// // `Sessions` is a gate whose identity is `Visitor`.
622    /// let app = App::with_gate(Sessions)
623    ///     .csrf(Csrf::new(*b"a persisted 32-byte secret key!!").unwrap())
624    ///     .route(get("/form", |cx: &mut Context<'_, Visitor>| {
625    ///         let token = cx.csrf_token()?;
626    ///         cx.html(200, &format!("<input type=hidden name=csrf_token value=\"{token}\">"))
627    ///     }).public())
628    ///     .build()?;
629    /// let r = serve_net::testing::respond(&app, b"GET /form HTTP/1.1\r\nHost: localhost\r\n\r\n");
630    /// assert!(r.text().starts_with("<input type=hidden name=csrf_token value=\""));
631    /// # Ok::<(), web_net::BuildError>(())
632    /// ```
633    ///
634    /// # Errors
635    ///
636    /// 500 `csrf.no_engine` without [`Builder::csrf`](crate::Builder::csrf);
637    /// 500 `csrf.no_binding` when the identity has no binding.
638    pub fn csrf_token(&self) -> Result<String> {
639        let engine = self
640            .shared
641            .csrf
642            .as_ref()
643            .ok_or_else(|| Error::internal("csrf.no_engine"))?;
644        let binding = self
645            .binding
646            .filter(|b| !b.is_empty())
647            .ok_or_else(|| Error::internal("csrf.no_binding"))?;
648        engine
649            .issue(binding)
650            .map(|t| t.as_str().to_owned())
651            .map_err(|e| Error::internal("csrf.issue").with_source(e))
652    }
653
654    /// Checks `If-Match` and `If-Unmodified-Since` against the current
655    /// version of what an unsafe request would change (RFC 9110 section
656    /// 13.2): `tag` is its entity tag's opaque part, `modified` its
657    /// modification time. Call it before changing anything.
658    ///
659    /// ```
660    /// use web_net::{App, put};
661    ///
662    /// let app = App::builder()
663    ///     .route(put("/doc", |cx| {
664    ///         cx.precondition(Some("v2"), None)?; // the stored version is "v2"
665    ///         cx.text(200, "saved")
666    ///     }).csrf_exempt("example"))
667    ///     .build()?;
668    /// let send = |tag: &str| serve_net::testing::respond(&app, format!("PUT /doc HTTP/1.1\r\nHost: localhost\r\nIf-Match: \"{tag}\"\r\nContent-Length: 0\r\n\r\n").as_bytes()).status;
669    /// assert_eq!(send("v1"), 412);
670    /// assert_eq!(send("v2"), 200);
671    /// # Ok::<(), web_net::BuildError>(())
672    /// ```
673    ///
674    /// # Errors
675    ///
676    /// 412 `precondition.failed` when a condition is false. Without either
677    /// field, `Ok`.
678    pub fn precondition(
679        &self,
680        tag: Option<&str>,
681        modified: Option<std::time::SystemTime>,
682    ) -> Result {
683        let current = tag.and_then(|t| parse_net::EntityTag::strong(t.as_bytes()).ok());
684        if let Some(v) = self.req.header("if-match") {
685            if !parse_net::if_match(v, current.as_ref()).unwrap_or(false) {
686                return Err(Error::new(412, "precondition.failed"));
687            }
688            return Ok(());
689        }
690        if let (Some(v), Some(m)) = (self.req.header("if-unmodified-since"), modified) {
691            let (Ok(since), Ok(lm)) = (
692                parse_net::http_date(v),
693                parse_net::HttpDate::from_system_time(m),
694            ) else {
695                return Ok(());
696            };
697            if lm > since {
698                return Err(Error::new(412, "precondition.failed"));
699            }
700        }
701        Ok(())
702    }
703
704    // ---------------------------------------------------------------- reply
705
706    /// Sets the status (default 200) for a reply written by
707    /// [`render`](Self::render) or [`reply_buffer`](Self::reply_buffer);
708    /// [`html`](Self::html), [`text`](Self::text), and [`bytes`](Self::bytes)
709    /// take theirs as an argument.
710    ///
711    /// ```
712    /// use web_net::{App, get};
713    ///
714    /// let app = App::builder()
715    ///     .route(get("/", |cx| {
716    ///         cx.status(202).set_header("content-type", "text/plain");
717    ///         cx.reply_buffer().extend_from_slice(b"queued");
718    ///         Ok(())
719    ///     }))
720    ///     .build()?;
721    /// let r = serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n");
722    /// assert_eq!((r.status, r.text()), (202, "queued"));
723    /// # Ok::<(), web_net::BuildError>(())
724    /// ```
725    pub fn status(&mut self, status: u16) -> &mut Self {
726        self.out.status = status;
727        self
728    }
729
730    /// Adds a reply field. A name that is not a token, a value with a
731    /// control character, or a framing field (`content-length`, ...) turns
732    /// the reply into a 500 when the handler returns.
733    ///
734    /// ```
735    /// use web_net::{App, get};
736    ///
737    /// let app = App::builder()
738    ///     .route(get("/", |cx| {
739    ///         cx.set_header("x-version", "7");
740    ///         cx.text(200, "ok")
741    ///     }))
742    ///     .route(get("/bad", |cx| {
743    ///         cx.set_header("content-length", "1"); // framing is serve-net's
744    ///         cx.text(200, "ok")
745    ///     }))
746    ///     .build()?;
747    /// let r = serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n");
748    /// assert_eq!(r.header("x-version"), Some(&b"7"[..]));
749    /// let r = serve_net::testing::respond(&app, b"GET /bad HTTP/1.1\r\nHost: localhost\r\n\r\n");
750    /// assert_eq!(r.status, 500);
751    /// # Ok::<(), web_net::BuildError>(())
752    /// ```
753    pub fn set_header(&mut self, name: &str, value: impl AsRef<[u8]>) -> &mut Self {
754        self.out.header(name, value.as_ref());
755        self
756    }
757
758    /// Status, `content_type`, and the body. A `content-type` the handler
759    /// set with [`set_header`](Self::set_header) wins.
760    ///
761    /// ```
762    /// use web_net::{App, get};
763    ///
764    /// let app = App::builder()
765    ///     .route(get("/logo.svg", |cx| cx.bytes(200, "image/svg+xml", b"<svg/>")))
766    ///     .build()?;
767    /// let r = serve_net::testing::respond(&app, b"GET /logo.svg HTTP/1.1\r\nHost: localhost\r\n\r\n");
768    /// assert_eq!(r.header("content-type"), Some(&b"image/svg+xml"[..]));
769    /// # Ok::<(), web_net::BuildError>(())
770    /// ```
771    ///
772    /// # Errors
773    ///
774    /// None; it returns [`Result`] so a handler can end with it.
775    pub fn bytes(&mut self, status: u16, content_type: &str, body: &[u8]) -> Result {
776        self.content_type(content_type);
777        self.out.status = status;
778        self.out.body.clear();
779        self.out.body.extend_from_slice(body);
780        Ok(())
781    }
782
783    /// Status, `content-type: text/html; charset=utf-8`, and the HTML.
784    ///
785    /// ```
786    /// use web_net::{App, get};
787    ///
788    /// let app = App::builder()
789    ///     .route(get("/", |cx| cx.html(200, "<h1>Hello</h1>")))
790    ///     .build()?;
791    /// let r = serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n");
792    /// assert_eq!((r.status, r.text()), (200, "<h1>Hello</h1>"));
793    /// # Ok::<(), web_net::BuildError>(())
794    /// ```
795    ///
796    /// # Errors
797    ///
798    /// None; it returns [`Result`] so a handler can end with it.
799    pub fn html(&mut self, status: u16, html: &str) -> Result {
800        self.bytes(status, "text/html; charset=utf-8", html.as_bytes())
801    }
802
803    /// Status, `content-type: text/plain; charset=utf-8`, and the text.
804    ///
805    /// ```
806    /// use web_net::{App, get};
807    ///
808    /// let app = App::builder().route(get("/", |cx| cx.text(200, "ok"))).build()?;
809    /// let r = serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n");
810    /// assert_eq!(r.text(), "ok");
811    /// # Ok::<(), web_net::BuildError>(())
812    /// ```
813    ///
814    /// # Errors
815    ///
816    /// None; it returns [`Result`] so a handler can end with it.
817    pub fn text(&mut self, status: u16, text: &str) -> Result {
818        self.bytes(status, "text/plain; charset=utf-8", text.as_bytes())
819    }
820
821    /// The reply body's buffer, emptied, for a writer that appends to a
822    /// `Vec<u8>` (a template engine with its own API writing straight in).
823    /// Set the status with [`status`](Self::status) and `content-type` with
824    /// [`set_header`](Self::set_header); the page budget does not apply here.
825    ///
826    /// ```
827    /// use std::io::Write;
828    /// use web_net::{App, get};
829    ///
830    /// let app = App::builder()
831    ///     .route(get("/", |cx| {
832    ///         write!(cx.reply_buffer(), "{} + {} = {}", 1, 2, 3).map_err(|_| web_net::Error::internal("write"))?;
833    ///         cx.set_header("content-type", "text/plain");
834    ///         Ok(())
835    ///     }))
836    ///     .build()?;
837    /// let r = serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n");
838    /// assert_eq!(r.text(), "1 + 2 = 3");
839    /// # Ok::<(), web_net::BuildError>(())
840    /// ```
841    pub fn reply_buffer(&mut self) -> &mut Vec<u8> {
842        self.out.body.clear();
843        &mut self.out.body
844    }
845
846    /// The best of `offers` for the request's `Accept` (`["text/html",
847    /// "application/json"]`), or `None` when the request accepts none of
848    /// them.
849    ///
850    /// ```
851    /// use web_net::{App, get};
852    ///
853    /// let app = App::builder()
854    ///     .route(get("/", |cx| match cx.accept(&["text/html", "application/json"]) {
855    ///         Some("application/json") => cx.bytes(200, "application/json", b"{}"),
856    ///         _ => cx.html(200, "<p>hi</p>"),
857    ///     }))
858    ///     .build()?;
859    /// let r = serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: localhost\r\nAccept: application/json\r\n\r\n");
860    /// assert_eq!(r.text(), "{}");
861    /// # Ok::<(), web_net::BuildError>(())
862    /// ```
863    #[must_use]
864    pub fn accept(&self, offers: &[&'static str]) -> Option<&'static str> {
865        let i = parse_net::best_media(self.req.header("accept"), offers)?;
866        offers.get(i).copied()
867    }
868
869    /// The best of `offers` for the request's `Accept-Language`
870    /// (`["en", "fr"]`), or `None` when the request states no preference
871    /// any offer meets.
872    ///
873    /// ```
874    /// use web_net::{App, get};
875    ///
876    /// let app = App::builder()
877    ///     .route(get("/", |cx| {
878    ///         let lang = cx.accept_language(&["en", "fr"]).unwrap_or("en");
879    ///         cx.text(200, lang)
880    ///     }))
881    ///     .build()?;
882    /// let r = serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: localhost\r\nAccept-Language: fr-CA, en;q=0.5\r\n\r\n");
883    /// assert_eq!(r.text(), "fr");
884    /// # Ok::<(), web_net::BuildError>(())
885    /// ```
886    #[must_use]
887    pub fn accept_language(&self, offers: &[&'static str]) -> Option<&'static str> {
888        let i = parse_net::best_language(self.req.header("accept-language"), offers)?;
889        offers.get(i).copied()
890    }
891
892    /// Adds a `Set-Cookie` field built with cookie-net (re-exported as
893    /// [`web_net::cookie_net`](crate::cookie_net)).
894    ///
895    /// ```
896    /// use web_net::{App, get, cookie_net::SetCookie};
897    ///
898    /// let app = App::builder()
899    ///     .route(get("/", |cx| {
900    ///         cx.set_cookie(&SetCookie::new("theme", "dark").map_err(|_| web_net::Error::internal("cookie"))?);
901    ///         cx.text(200, "ok")
902    ///     }))
903    ///     .build()?;
904    /// let r = serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n");
905    /// assert!(r.header("set-cookie").is_some());
906    /// # Ok::<(), web_net::BuildError>(())
907    /// ```
908    pub fn set_cookie(&mut self, cookie: &cookie_net::SetCookie<'_>) -> &mut Self {
909        let line = cookie.encode();
910        self.out.header("set-cookie", line.as_bytes());
911        self
912    }
913
914    /// The conditional fast path for a page: sets `ETag: "<tag>"` and, when
915    /// the request's `If-None-Match` matches it (a `GET` or `HEAD`), turns the
916    /// reply into `304 Not Modified` and returns `true`, so the handler
917    /// returns before building the page.
918    ///
919    /// `tag` is the opaque part (letters, digits, and `-._~` are safe), for
920    /// example a revision number. An unusable tag sets nothing and returns
921    /// `false`.
922    ///
923    /// ```
924    /// use web_net::{App, get};
925    ///
926    /// let app = App::builder()
927    ///     .route(get("/", |cx| {
928    ///         if cx.not_modified("rev-7") {
929    ///             return Ok(());
930    ///         }
931    ///         cx.html(200, "<h1>page</h1>")
932    ///     }))
933    ///     .build()?;
934    /// let r = serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: localhost\r\nIf-None-Match: \"rev-7\"\r\n\r\n");
935    /// assert_eq!(r.status, 304);
936    /// # Ok::<(), web_net::BuildError>(())
937    /// ```
938    pub fn not_modified(&mut self, tag: &str) -> bool {
939        let Ok(current) = parse_net::EntityTag::strong(tag.as_bytes()) else {
940            return false;
941        };
942        let quoted = format!("\"{tag}\"");
943        self.out.header("etag", quoted.as_bytes());
944        if !matches!(self.method(), Method::Get | Method::Head) {
945            return false;
946        }
947        let matched = self
948            .req
949            .header("if-none-match")
950            .is_some_and(|v| parse_net::if_none_match(v, &current).unwrap_or(false));
951        if matched {
952            self.out.status = 304;
953            self.out.body.clear();
954        }
955        matched
956    }
957
958    fn content_type(&mut self, value: &str) {
959        if !self.out.has_type {
960            self.out.header("content-type", value.as_bytes());
961        }
962    }
963
964    /// The site path `path` as a URL path: the site's base path in front
965    /// (`/blog` + `/posts/1`), so the site works installed in a folder.
966    /// `path` is used as it is (a query and a fragment included); for a
967    /// path built from a visitor's input, use [`url_path`](Self::url_path),
968    /// which encodes each segment.
969    ///
970    /// ```
971    /// use web_net::{App, get};
972    ///
973    /// let app = App::builder()
974    ///     .base("/blog")
975    ///     .route(get("/", |cx| {
976    ///         let about = cx.url("/about")?;
977    ///         cx.html(200, &format!("<a href=\"{about}\">about</a>"))
978    ///     }))
979    ///     .build()?;
980    /// let r = serve_net::testing::respond(&app, b"GET /blog/ HTTP/1.1\r\nHost: localhost\r\n\r\n");
981    /// assert_eq!(r.text(), "<a href=\"/blog/about\">about</a>");
982    /// # Ok::<(), web_net::BuildError>(())
983    /// ```
984    ///
985    /// # Errors
986    ///
987    /// 500 `url.not_local` when `path` is not a path of this site: it must
988    /// start with one `/` (never `//` or `/\`, which browsers read as
989    /// another host) and hold only visible ASCII.
990    pub fn url(&self, path: &str) -> Result<String> {
991        if !is_local_target(path) {
992            return Err(Error::internal("url.not_local"));
993        }
994        Ok(site_url(&self.shared.base, path))
995    }
996
997    /// A URL path from segments, each percent-encoded as a path segment, the
998    /// base path in front: `["tags", tag]` gives `/blog/tags/a%2Fb` for the
999    /// tag `a/b`. A segment cannot add a path level, a query, a fragment, or
1000    /// another host (`/`, `?`, `#`, `%`, and spaces are encoded; characters a
1001    /// path segment allows, such as `&` and `+`, stay).
1002    ///
1003    /// ```
1004    /// use web_net::{App, get};
1005    ///
1006    /// let app = App::builder()
1007    ///     .route(get("/", |cx| cx.text(200, &cx.url_path(&["tags", "c# & c++"]))))
1008    ///     .build()?;
1009    /// let r = serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n");
1010    /// assert_eq!(r.text(), "/tags/c%23%20&%20c++");
1011    /// # Ok::<(), web_net::BuildError>(())
1012    /// ```
1013    #[must_use]
1014    pub fn url_path(&self, segments: &[&str]) -> String {
1015        let mut out = String::from(&*self.shared.base);
1016        for s in segments {
1017            out.push('/');
1018            url::encode_segment(s.as_bytes(), &mut out);
1019        }
1020        if out.is_empty() || segments.is_empty() {
1021            out.push('/');
1022        }
1023        out
1024    }
1025
1026    /// The page a sign-in redirect asked for: the `next` query parameter
1027    /// web-net puts on the redirect to the sign-in page, as a site-relative
1028    /// path safe to pass to [`redirect`](Self::redirect) once the visitor
1029    /// signed in. `None` when there is none, or when it is not a path of this
1030    /// site (an attacker's link to the sign-in page cannot turn it into an
1031    /// open redirect).
1032    ///
1033    /// ```
1034    /// use web_net::{App, get};
1035    ///
1036    /// let app = App::builder()
1037    ///     .base("/app")
1038    ///     .route(get("/login", |cx| {
1039    ///         let next = cx.next_path()?.unwrap_or_else(|| "/".into());
1040    ///         cx.text(200, &next)
1041    ///     }))
1042    ///     .build()?;
1043    /// let at = |q: &str| serve_net::testing::respond(&app, format!("GET /app/login?{q} HTTP/1.1\r\nHost: localhost\r\n\r\n").as_bytes()).text().to_owned();
1044    /// assert_eq!(at("next=/app/account%3Ftab%3D2"), "/account?tab=2");
1045    /// assert_eq!(at("next=//evil.example"), "/");
1046    /// assert_eq!(at("next=/elsewhere"), "/");
1047    /// # Ok::<(), web_net::BuildError>(())
1048    /// ```
1049    ///
1050    /// # Errors
1051    ///
1052    /// As [`query`](Self::query): 400 when `next` is given twice or does not
1053    /// decode.
1054    pub fn next_path(&self) -> Result<Option<String>> {
1055        let Some(next) = self.query("next")? else {
1056            return Ok(None);
1057        };
1058        let base = self.shared.base.as_ref();
1059        let rel = if base.is_empty() {
1060            next.as_ref()
1061        } else {
1062            match next.strip_prefix(base) {
1063                Some("") => "/",
1064                Some(r) if r.starts_with('/') => r,
1065                _ => return Ok(None),
1066            }
1067        };
1068        Ok(is_local_target(rel).then(|| rel.to_owned()))
1069    }
1070
1071    /// A redirect to another site, only to a host listed with
1072    /// [`Builder::redirect_hosts`](crate::Builder::redirect_hosts):
1073    /// `https://` (or `http://`), the host, and a visible-ASCII rest.
1074    ///
1075    /// ```
1076    /// use web_net::{App, get};
1077    ///
1078    /// let app = App::builder()
1079    ///     .redirect_hosts(&["docs.example.com"])
1080    ///     .route(get("/docs", |cx| cx.redirect_external(302, "https://docs.example.com/start")))
1081    ///     .route(get("/away", |cx| cx.redirect_external(302, "https://elsewhere.example/")))
1082    ///     .build()?;
1083    /// let r = serve_net::testing::respond(&app, b"GET /docs HTTP/1.1\r\nHost: localhost\r\n\r\n");
1084    /// assert_eq!(r.header("location"), Some(&b"https://docs.example.com/start"[..]));
1085    /// let r = serve_net::testing::respond(&app, b"GET /away HTTP/1.1\r\nHost: localhost\r\n\r\n");
1086    /// assert_eq!(r.status, 500);
1087    /// # Ok::<(), web_net::BuildError>(())
1088    /// ```
1089    ///
1090    /// # Errors
1091    ///
1092    /// 500 `redirect.not_allowed` for any other target, 500
1093    /// `redirect.status` for a status that is not a redirect.
1094    pub fn redirect_external(&mut self, status: u16, url: &str) -> Result {
1095        if !matches!(status, 301 | 302 | 303 | 307 | 308) {
1096            return Err(Error::internal("redirect.status"));
1097        }
1098        let rest = url
1099            .strip_prefix("https://")
1100            .or_else(|| url.strip_prefix("http://"))
1101            .ok_or_else(|| Error::internal("redirect.not_allowed"))?;
1102        let end = rest.find(['/', '?', '#']).unwrap_or(rest.len());
1103        let authority = rest[..end].to_ascii_lowercase();
1104        let host = authority
1105            .rsplit_once(':')
1106            .map_or(authority.as_str(), |(h, p)| {
1107                if p.bytes().all(|b| b.is_ascii_digit()) && !p.is_empty() {
1108                    h
1109                } else {
1110                    ""
1111                }
1112            });
1113        let listed = self
1114            .shared
1115            .redirect_hosts
1116            .iter()
1117            .any(|h| h.as_ref() == authority || h.as_ref() == host);
1118        let visible = url.bytes().all(|c| (0x21..0x7f).contains(&c) && c != b'\\');
1119        if !listed || !visible || authority.contains('@') {
1120            return Err(Error::internal("redirect.not_allowed"));
1121        }
1122        self.out.header("location", url.as_bytes());
1123        self.out.status = status;
1124        self.out.body.clear();
1125        Ok(())
1126    }
1127
1128    /// A redirect with `status` to a site-relative path, the site's base
1129    /// path put in front. 303 answers a form that was accepted, so a reload
1130    /// does not post it again; 301 and 308 are permanent; 302 and 307
1131    /// temporary.
1132    ///
1133    /// ```
1134    /// use web_net::{App, post};
1135    ///
1136    /// let app = App::builder()
1137    ///     .base("/app")
1138    ///     .route(post("/contact", |cx| cx.redirect(303, "/thanks")).csrf_exempt("example"))
1139    ///     .build()?;
1140    /// let r = serve_net::testing::respond(&app, b"POST /app/contact HTTP/1.1\r\nHost: localhost\r\nContent-Length: 0\r\n\r\n");
1141    /// assert_eq!((r.status, r.header("location")), (303, Some(&b"/app/thanks"[..])));
1142    /// # Ok::<(), web_net::BuildError>(())
1143    /// ```
1144    ///
1145    /// # Errors
1146    ///
1147    /// 500 `redirect.not_local` when `path` is not a local path (it must
1148    /// start with one `/`): redirects to other hosts are never built from a
1149    /// string a handler passes along (use
1150    /// [`redirect_external`](Self::redirect_external)). 500
1151    /// `redirect.status` for a status that is not 301, 302, 303, 307, or 308.
1152    pub fn redirect(&mut self, status: u16, path: &str) -> Result {
1153        if !matches!(status, 301 | 302 | 303 | 307 | 308) {
1154            return Err(Error::internal("redirect.status"));
1155        }
1156        if !is_local_target(path) {
1157            return Err(Error::internal("redirect.not_local"));
1158        }
1159        let location = site_url(&self.shared.base, path);
1160        self.out.header("location", location.as_bytes());
1161        self.out.status = status;
1162        self.out.body.clear();
1163        Ok(())
1164    }
1165
1166    /// Renders the route's template (declared with
1167    /// [`Route::template`](crate::Route::template)) against `view` into the
1168    /// body. The status stays as set (200 by default); `content-type` is
1169    /// HTML unless the handler set one.
1170    ///
1171    /// ```
1172    /// use web_net::{App, Context, Guest, Output, Render, RenderError, TemplateId, get};
1173    ///
1174    /// struct Page<'a> { title: &'a str }
1175    /// struct Pages;
1176    /// impl Render for Pages {
1177    ///     type View<'a> = Page<'a>;
1178    ///     fn template(&self, name: &str) -> Option<TemplateId> {
1179    ///         (name == "page").then_some(TemplateId(0))
1180    ///     }
1181    ///     fn render(&self, _: TemplateId, page: &Page<'_>, out: &mut Output<'_>) -> Result<(), RenderError> {
1182    ///         out.write_str(page.title) // a real engine escapes
1183    ///     }
1184    /// }
1185    ///
1186    /// let app = App::builder()
1187    ///     .render(Pages)
1188    ///     .route(get("/", |cx: &mut Context<'_, Guest, Pages>| cx.render(&Page { title: "Home" })).template("page"))
1189    ///     .build()?;
1190    /// let r = serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n");
1191    /// assert_eq!(r.text(), "Home");
1192    /// assert_eq!(r.header("content-type"), Some(&b"text/html; charset=utf-8"[..]));
1193    /// # Ok::<(), web_net::BuildError>(())
1194    /// ```
1195    ///
1196    /// # Errors
1197    ///
1198    /// 500 `render.no_engine` or `render.no_template` when nothing can
1199    /// render; the engine's own [`RenderError`](crate::RenderError) as a 500;
1200    /// a checkpoint's denial.
1201    pub fn render(&mut self, view: &E::View<'_>) -> Result {
1202        let t = self
1203            .template
1204            .ok_or_else(|| Error::internal("render.no_template"))?;
1205        self.render_id(t, view)
1206    }
1207
1208    /// Renders the template `name` against `view`. The name is resolved by
1209    /// the engine on every call; prefer [`render`](Self::render) with a
1210    /// template declared on the route, resolved once at build.
1211    ///
1212    /// ```
1213    /// use web_net::{App, Context, Guest, Output, Render, RenderError, TemplateId, get};
1214    ///
1215    /// struct Page<'a> { title: &'a str }
1216    /// struct Pages;
1217    /// impl Render for Pages {
1218    ///     type View<'a> = Page<'a>;
1219    ///     fn template(&self, name: &str) -> Option<TemplateId> {
1220    ///         (name == "page").then_some(TemplateId(0))
1221    ///     }
1222    ///     fn render(&self, _: TemplateId, page: &Page<'_>, out: &mut Output<'_>) -> Result<(), RenderError> {
1223    ///         out.write_str(page.title) // a real engine escapes
1224    ///     }
1225    /// }
1226    ///
1227    /// let app = App::builder()
1228    ///     .render(Pages)
1229    ///     .route(get("/", |cx: &mut Context<'_, Guest, Pages>| cx.render_named("page", &Page { title: "Named" })))
1230    ///     .build()?;
1231    /// let r = serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n");
1232    /// assert_eq!(r.text(), "Named");
1233    /// # Ok::<(), web_net::BuildError>(())
1234    /// ```
1235    ///
1236    /// # Errors
1237    ///
1238    /// As [`render`](Self::render).
1239    pub fn render_named(&mut self, name: &str, view: &E::View<'_>) -> Result {
1240        let engine = self
1241            .shared
1242            .render
1243            .as_ref()
1244            .ok_or_else(|| Error::internal("render.no_engine"))?;
1245        let t = engine
1246            .template(name)
1247            .ok_or_else(|| Error::internal("render.no_template"))?;
1248        self.render_id(t, view)
1249    }
1250
1251    fn render_id(&mut self, t: TemplateId, view: &E::View<'_>) -> Result {
1252        let engine = self
1253            .shared
1254            .render
1255            .as_ref()
1256            .ok_or_else(|| Error::internal("render.no_engine"))?;
1257        if !self.rendering {
1258            // Hooks may render too; they do not run their own hooks again.
1259            self.rendering = true;
1260            let hooks = self.before_render;
1261            let ran = hooks.iter().try_for_each(|h| h(self));
1262            self.rendering = false;
1263            ran?;
1264        }
1265        if let Some(cp) = &self.shared.checkpoint {
1266            match cp.check(self.req.env, Stage::BeforeRender) {
1267                Verdict::Pass => {}
1268                Verdict::Deny { status } => {
1269                    // As at the authorized stage: an error status, or 403.
1270                    let status = if (400..=599).contains(&status) {
1271                        status
1272                    } else {
1273                        403
1274                    };
1275                    return Err(Error::new(status, "checkpoint.denied"));
1276                }
1277                _ => return Err(Error::forbidden("checkpoint.killed")),
1278            }
1279        }
1280        self.out.body.clear();
1281        let mut output = Output::new(&mut self.out.body, self.shared.page_limit);
1282        let rendered = engine.render(t, view, &mut output);
1283        let over = output.over();
1284        if let Err(e) = rendered {
1285            self.out.body.clear();
1286            return Err(e.into());
1287        }
1288        if over {
1289            self.out.body.clear();
1290            return Err(crate::RenderError::new("render.too_large").into());
1291        }
1292        self.content_type("text/html; charset=utf-8");
1293        Ok(())
1294    }
1295}
1296
1297impl<I, E> Context<'_, I, E>
1298where
1299    E: for<'a> Render<View<'a> = dyn View + 'a>,
1300{
1301    /// [`render`](Self::render) for an engine that reads any [`View`] (a
1302    /// dynamic engine): takes `&dyn View`, so a [`Fields`](crate::Fields)
1303    /// or any view of yours goes in as it is.
1304    ///
1305    /// ```
1306    /// use web_net::{App, Fields, Output, Render, RenderError, TemplateId, Value, View, get};
1307    ///
1308    /// /// Writes the view's `title` field.
1309    /// struct Echo;
1310    /// impl Render for Echo {
1311    ///     type View<'a> = dyn View + 'a;
1312    ///     fn template(&self, _: &str) -> Option<TemplateId> { Some(TemplateId(0)) }
1313    ///     fn render(&self, _: TemplateId, view: &dyn View, out: &mut Output<'_>) -> Result<(), RenderError> {
1314    ///         let key = view.shape().key("title").ok_or(RenderError::new("echo.no_title"))?;
1315    ///         out.write_str(view.get(key).as_text().unwrap_or("")) // a real engine escapes
1316    ///     }
1317    /// }
1318    ///
1319    /// let app = App::builder()
1320    ///     .render(Echo)
1321    ///     .route(get("/", |cx| cx.render_view(&Fields(&[("title", Value::Text("Home"))]))).template("page"))
1322    ///     .build()?;
1323    /// let r = serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n");
1324    /// assert_eq!(r.text(), "Home");
1325    /// # Ok::<(), web_net::BuildError>(())
1326    /// ```
1327    ///
1328    /// # Errors
1329    ///
1330    /// As [`render`](Self::render).
1331    pub fn render_view(&mut self, view: &dyn View) -> Result {
1332        self.render(view)
1333    }
1334
1335    /// [`render_named`](Self::render_named) for an engine that reads any
1336    /// [`View`].
1337    ///
1338    /// ```
1339    /// use web_net::{App, Fields, Output, Render, RenderError, TemplateId, Value, View, get};
1340    ///
1341    /// /// Writes the view's `title` field.
1342    /// struct Echo;
1343    /// impl Render for Echo {
1344    ///     type View<'a> = dyn View + 'a;
1345    ///     fn template(&self, _: &str) -> Option<TemplateId> { Some(TemplateId(0)) }
1346    ///     fn render(&self, _: TemplateId, view: &dyn View, out: &mut Output<'_>) -> Result<(), RenderError> {
1347    ///         let key = view.shape().key("title").ok_or(RenderError::new("echo.no_title"))?;
1348    ///         out.write_str(view.get(key).as_text().unwrap_or("")) // a real engine escapes
1349    ///     }
1350    /// }
1351    ///
1352    /// let app = App::builder()
1353    ///     .render(Echo)
1354    ///     .route(get("/", |cx| cx.render_view_named("page", &Fields(&[("title", Value::Text("Named"))]))))
1355    ///     .build()?;
1356    /// let r = serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n");
1357    /// assert_eq!(r.text(), "Named");
1358    /// # Ok::<(), web_net::BuildError>(())
1359    /// ```
1360    ///
1361    /// # Errors
1362    ///
1363    /// As [`render`](Self::render).
1364    pub fn render_view_named(&mut self, name: &str, view: &dyn View) -> Result {
1365        self.render_named(name, view)
1366    }
1367}
1368
1369impl<I, E: Render> fmt::Debug for Context<'_, I, E> {
1370    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1371        f.debug_struct("Context")
1372            .field("request", &self.req)
1373            .field("status", &self.out.status)
1374            .finish_non_exhaustive()
1375    }
1376}
1377
1378/// Decodes one query name or value: `+` is a space, escapes decoded once.
1379fn decode_query(s: &str) -> Option<Cow<'_, str>> {
1380    if !s.contains(['%', '+']) {
1381        return Some(Cow::Borrowed(s));
1382    }
1383    let b = s.as_bytes();
1384    let mut out = Vec::with_capacity(b.len());
1385    let mut i = 0;
1386    while i < b.len() {
1387        match b[i] {
1388            b'+' => {
1389                out.push(b' ');
1390                i += 1;
1391            }
1392            b'%' => {
1393                let h = b.get(i + 1).and_then(|&c| (c as char).to_digit(16))?;
1394                let l = b.get(i + 2).and_then(|&c| (c as char).to_digit(16))?;
1395                out.push(((h << 4) | l) as u8);
1396                i += 3;
1397            }
1398            c => {
1399                out.push(c);
1400                i += 1;
1401            }
1402        }
1403    }
1404    String::from_utf8(out).ok().map(Cow::Owned)
1405}
1406
1407#[cfg(test)]
1408mod tests {
1409    use super::*;
1410
1411    #[test]
1412    fn test_local_targets() {
1413        for ok in ["/", "/a", "/a/b?c=d", "/a%20b"] {
1414            assert!(is_local_target(ok), "{ok}");
1415        }
1416        for bad in [
1417            "",
1418            "a",
1419            "//evil.example",
1420            "/\\evil.example",
1421            "https://x",
1422            "/a\r\nx",
1423            "/a b",
1424            "/a\\b",
1425        ] {
1426            assert!(!is_local_target(bad), "{bad:?}");
1427        }
1428    }
1429
1430    #[test]
1431    fn test_decode_query() {
1432        assert_eq!(decode_query("a+b%21").as_deref(), Some("a b!"));
1433        assert_eq!(decode_query("plain").as_deref(), Some("plain"));
1434        assert_eq!(decode_query("%zz"), None);
1435        assert_eq!(decode_query("%ff"), None);
1436    }
1437
1438    #[test]
1439    fn test_out_refuses_bad_fields() {
1440        let mut o = Out::default();
1441        o.header("x-ok", b"v");
1442        assert!(!o.invalid);
1443        o.header("content-length", b"1");
1444        assert!(o.invalid);
1445        let mut o = Out::default();
1446        o.header("x", b"a\r\nb");
1447        assert!(o.invalid);
1448        let mut o = Out::default();
1449        o.header("bad name", b"v");
1450        assert!(o.invalid);
1451        let mut o = Out::default();
1452        o.header("Content-Type", b"text/x");
1453        assert!(o.has_type && o.has("content-type"));
1454    }
1455}