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::{Output, 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/// A site-relative path that is safe as a redirect target: it starts with
120/// one `/` (never `//` or `/\`, which browsers read as another host) and
121/// holds only visible ASCII.
122pub(crate) fn is_local_target(p: &str) -> bool {
123    let b = p.as_bytes();
124    b.first() == Some(&b'/')
125        && !matches!(b.get(1), Some(b'/' | b'\\'))
126        && b.iter().all(|&c| (0x21..0x7f).contains(&c) && c != b'\\')
127}
128
129/// The context a handler receives: proof that the request passed its
130/// route's access check, the identity it carries, and the reply.
131///
132/// There is no public way to make one: web-net builds it only after the
133/// gate cleared the request.
134pub struct Cx<'r, I> {
135    pub(crate) req: Request<'r>,
136    pub(crate) who: &'r I,
137    pub(crate) caps: Captures,
138    pub(crate) names: &'r [Box<str>],
139    pub(crate) out: &'r mut Out,
140    pub(crate) shared: &'r Shared,
141    pub(crate) template: Option<TemplateId>,
142    pub(crate) binding: Option<&'r [u8]>,
143    pub(crate) limits: &'r form_net::Limits,
144    pub(crate) before_render: &'r [crate::app::HookFn<I>],
145    pub(crate) rendering: bool,
146}
147
148impl<'r, I> Cx<'r, I> {
149    // ---------------------------------------------------------------- request
150
151    /// The request.
152    #[must_use]
153    pub fn request(&self) -> &Request<'r> {
154        &self.req
155    }
156
157    /// The method.
158    #[must_use]
159    pub fn method(&self) -> Method {
160        self.req.method()
161    }
162
163    /// The canonical path inside the site.
164    #[must_use]
165    pub fn path(&self) -> &'r str {
166        self.req.path
167    }
168
169    /// The first value of a request field.
170    #[must_use]
171    pub fn header(&self, name: &str) -> Option<&'r [u8]> {
172        self.req.header(name)
173    }
174
175    /// Who the request comes from: always present, possibly a guest.
176    #[must_use]
177    pub fn identity(&self) -> &'r I {
178        self.who
179    }
180
181    /// The request's reference, as shown on error pages.
182    #[must_use]
183    pub fn id(&self) -> RequestId {
184        self.req.id()
185    }
186
187    /// A route parameter (`:id` or `*rest`), percent-decoded.
188    #[must_use]
189    pub fn param(&self, name: &str) -> Option<Cow<'r, str>> {
190        let i = self.names.iter().position(|n| n.as_ref() == name)?;
191        let (s, e) = self.caps.get(i)?;
192        Some(url::decode(&self.req.path[s..e]))
193    }
194
195    /// A route parameter parsed as `T`. A value that does not parse means
196    /// the page does not exist (`/users/abc` for a numeric id): 404.
197    ///
198    /// # Errors
199    ///
200    /// 404 `route.param` when the parameter is missing or does not parse.
201    pub fn param_as<T: FromStr>(&self, name: &str) -> Result<T> {
202        self.param(name)
203            .and_then(|v| v.parse().ok())
204            .ok_or_else(|| Error::not_found("route.param"))
205    }
206
207    /// The first value of a query parameter, decoded (`+` is a space).
208    ///
209    /// # Errors
210    ///
211    /// 400 `query.duplicate` when the name appears twice (parameter
212    /// pollution is refused, never resolved by picking one), 400
213    /// `query.invalid` when the value does not decode to UTF-8.
214    pub fn query(&self, name: &str) -> Result<Option<Cow<'r, str>>> {
215        let Some(q) = self.req.query() else {
216            return Ok(None);
217        };
218        let mut found = None;
219        for pair in q.split('&') {
220            let (k, v) = pair.split_once('=').unwrap_or((pair, ""));
221            if decode_query(k).as_deref() != Some(name) {
222                continue;
223            }
224            if found.is_some() {
225                return Err(Error::bad_request("query.duplicate"));
226            }
227            found = Some(decode_query(v).ok_or_else(|| Error::bad_request("query.invalid"))?);
228        }
229        Ok(found)
230    }
231
232    /// The raw request body. On an unsafe method its CSRF token was
233    /// checked before the handler ran.
234    ///
235    /// # Errors
236    ///
237    /// None today; reading the body stays fallible so a content inspector
238    /// can refuse it in a later version without changing handlers.
239    pub fn body(&self) -> Result<&'r [u8]> {
240        Ok(self.req.http.body_bytes())
241    }
242
243    /// Reads the body as a typed form (urlencoded or multipart text fields),
244    /// with the route's [`form_limits`](crate::Route::form_limits).
245    ///
246    /// On an unsafe method the CSRF token was checked before the handler
247    /// ran. A form type that is posted with a token lists the token field in
248    /// its `ignore` list (`ignore = ["csrf_token"]`).
249    ///
250    /// The error keeps form-net's detail, so an action can render the form
251    /// again with 422; `?` turns it into an [`Error`] with its status.
252    ///
253    /// ```
254    /// use web_net::form_net::{self, FromForm};
255    ///
256    /// form_net::form! {
257    ///     pub struct Signup { pub email: String }
258    ///     ignore = ["csrf_token"];
259    /// }
260    ///
261    /// let app = web_net::App::builder()
262    ///     .route(web_net::post("/signup", |cx| {
263    ///         let form: Signup = cx.form()?;
264    ///         cx.text(200, &form.email);
265    ///         Ok(())
266    ///     }).public().csrf_exempt("example without a session"))
267    ///     .build()?;
268    /// let r = serve_net::testing::respond(&app,
269    ///     b"POST /signup HTTP/1.1\r\nHost: a\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: 15\r\n\r\nemail=a%40b.com");
270    /// assert_eq!(r.text(), "a@b.com");
271    /// # Ok::<(), web_net::BuildError>(())
272    /// ```
273    ///
274    /// # Errors
275    ///
276    /// form-net's [`FormError`](form_net::FormError): 415 for another media
277    /// type, 413 for a budget passed, 400 for malformed input, 422 for
278    /// fields that do not fit `T`, 403 for a CSRF token.
279    pub fn form<T: form_net::FromForm>(&mut self) -> std::result::Result<T, form_net::FormError> {
280        self.form_inspected(&mut form_net::NoInspector)
281    }
282
283    /// As [`form`](Self::form), with an inspector beside the parse (a
284    /// content policy that sees every part and field first).
285    ///
286    /// # Errors
287    ///
288    /// As [`form`](Self::form), plus the inspector's refusals.
289    pub fn form_inspected<T: form_net::FromForm, N: form_net::Inspector>(
290        &mut self,
291        inspector: &mut N,
292    ) -> std::result::Result<T, form_net::FormError> {
293        let content_type = self.req.header("content-type").unwrap_or(b"");
294        let body = self.req.http.body_bytes();
295        T::from_body_inspected(
296            content_type,
297            body,
298            self.limits,
299            &form_net::Policy::DEFAULT,
300            inspector,
301        )
302    }
303
304    /// Reads a multipart upload into `store` (feature `uploads`): text
305    /// fields in memory, files in the store's private temporary files, with
306    /// the route's form limits. The CSRF token was checked before the
307    /// handler ran, so a forged one stores nothing. Turn the result into a typed form with
308    /// `T::from_upload(upload)`.
309    ///
310    /// # Errors
311    ///
312    /// As [`form`](Self::form), plus the store's errors.
313    #[cfg(feature = "uploads")]
314    #[cfg_attr(docsrs, doc(cfg(feature = "uploads")))]
315    pub fn upload(
316        &mut self,
317        store: &form_net::Store,
318    ) -> std::result::Result<form_net::Upload, form_net::FormError> {
319        self.upload_inspected(store, &mut form_net::NoInspector)
320    }
321
322    /// As [`upload`](Self::upload), with an inspector beside the parse.
323    ///
324    /// # Errors
325    ///
326    /// As [`upload`](Self::upload), plus the inspector's refusals.
327    #[cfg(feature = "uploads")]
328    #[cfg_attr(docsrs, doc(cfg(feature = "uploads")))]
329    pub fn upload_inspected<N: form_net::Inspector>(
330        &mut self,
331        store: &form_net::Store,
332        inspector: &mut N,
333    ) -> std::result::Result<form_net::Upload, form_net::FormError> {
334        let content_type = self.req.header("content-type").unwrap_or(b"");
335        let body = self.req.http.body_bytes();
336        let mut session = form_net::UploadSession::new(
337            content_type,
338            self.limits,
339            &form_net::Policy::DEFAULT,
340            store,
341        )?;
342        session.push(body, inspector)?;
343        session.finish(inspector)
344    }
345
346    /// A CSRF token for the visitor's session, to put in a form's hidden
347    /// `csrf_token` field or a script's `X-CSRF-Token` header. Different on
348    /// every call (masked), so a compression side channel cannot recover it.
349    ///
350    /// # Errors
351    ///
352    /// 500 `csrf.no_engine` without [`Builder::csrf`](crate::Builder::csrf);
353    /// 500 `csrf.no_binding` when the identity has no binding.
354    pub fn csrf_token(&self) -> Result<String> {
355        let engine = self
356            .shared
357            .csrf
358            .as_ref()
359            .ok_or_else(|| Error::internal("csrf.no_engine"))?;
360        let binding = self
361            .binding
362            .filter(|b| !b.is_empty())
363            .ok_or_else(|| Error::internal("csrf.no_binding"))?;
364        engine
365            .issue(binding)
366            .map(|t| t.as_str().to_owned())
367            .map_err(|e| Error::internal("csrf.issue").with_source(e))
368    }
369
370    /// Checks `If-Match` and `If-Unmodified-Since` against the current
371    /// version of what an unsafe request would change (RFC 9110 section
372    /// 13.2): `tag` is its entity tag's opaque part, `modified` its
373    /// modification time. Call it before changing anything.
374    ///
375    /// # Errors
376    ///
377    /// 412 `precondition.failed` when a condition is false. Without either
378    /// field, `Ok`.
379    pub fn precondition(
380        &self,
381        tag: Option<&str>,
382        modified: Option<std::time::SystemTime>,
383    ) -> Result {
384        let current = tag.and_then(|t| parse_net::EntityTag::strong(t.as_bytes()).ok());
385        if let Some(v) = self.req.header("if-match") {
386            if !parse_net::if_match(v, current.as_ref()).unwrap_or(false) {
387                return Err(Error::new(412, "precondition.failed"));
388            }
389            return Ok(());
390        }
391        if let (Some(v), Some(m)) = (self.req.header("if-unmodified-since"), modified) {
392            let (Ok(since), Ok(lm)) = (
393                parse_net::http_date(v),
394                parse_net::HttpDate::from_system_time(m),
395            ) else {
396                return Ok(());
397            };
398            if lm > since {
399                return Err(Error::new(412, "precondition.failed"));
400            }
401        }
402        Ok(())
403    }
404
405    // ---------------------------------------------------------------- reply
406
407    /// Sets the status (default 200).
408    pub fn status(&mut self, status: u16) -> &mut Self {
409        self.out.status = status;
410        self
411    }
412
413    /// Adds a reply field. A name that is not a token, a value with a
414    /// control character, or a framing field (`content-length`, ...) turns
415    /// the reply into a 500 when the handler returns.
416    pub fn set_header(&mut self, name: &str, value: impl AsRef<[u8]>) -> &mut Self {
417        self.out.header(name, value.as_ref());
418        self
419    }
420
421    /// Sets the body.
422    pub fn body_bytes(&mut self, bytes: &[u8]) -> &mut Self {
423        self.out.body.clear();
424        self.out.body.extend_from_slice(bytes);
425        self
426    }
427
428    /// The best of `offers` for the request's `Accept-Language`
429    /// (`["en", "fr"]`), or `None` when the request states no preference
430    /// any offer meets.
431    #[must_use]
432    pub fn language(&self, offers: &[&'static str]) -> Option<&'static str> {
433        let i = parse_net::best_language(self.req.header("accept-language"), offers)?;
434        offers.get(i).copied()
435    }
436
437    /// The best of `offers` for the request's `Accept` (`["text/html",
438    /// "application/json"]`).
439    #[must_use]
440    pub fn media(&self, offers: &[&'static str]) -> Option<&'static str> {
441        let i = parse_net::best_media(self.req.header("accept"), offers)?;
442        offers.get(i).copied()
443    }
444
445    /// The reply body's buffer, emptied, for a writer that appends to a
446    /// `Vec<u8>` (a template engine with its own API writing straight in).
447    /// Set `content-type` yourself; the page budget does not apply here.
448    pub fn body_mut(&mut self) -> &mut Vec<u8> {
449        self.out.body.clear();
450        &mut self.out.body
451    }
452
453    /// Status, `content-type: text/html; charset=utf-8`, and the HTML.
454    pub fn html(&mut self, status: u16, html: &str) -> &mut Self {
455        self.content_type("text/html; charset=utf-8");
456        self.status(status).body_bytes(html.as_bytes())
457    }
458
459    /// Status, `content-type: text/plain; charset=utf-8`, and the text.
460    pub fn text(&mut self, status: u16, text: &str) -> &mut Self {
461        self.content_type("text/plain; charset=utf-8");
462        self.status(status).body_bytes(text.as_bytes())
463    }
464
465    /// Adds a `Set-Cookie` field built with cookie-net (re-exported as
466    /// [`web_net::cookie_net`](crate::cookie_net)).
467    ///
468    /// ```
469    /// use web_net::{App, get, cookie_net::SetCookie};
470    ///
471    /// let app = App::builder()
472    ///     .route(get("/", |cx| {
473    ///         cx.set_cookie(&SetCookie::new("theme", "dark").map_err(|_| web_net::Error::internal("cookie"))?);
474    ///         cx.text(200, "ok");
475    ///         Ok(())
476    ///     }).public())
477    ///     .build()?;
478    /// let r = serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: a\r\n\r\n");
479    /// assert!(r.header("set-cookie").is_some());
480    /// # Ok::<(), web_net::BuildError>(())
481    /// ```
482    pub fn set_cookie(&mut self, cookie: &cookie_net::SetCookie<'_>) -> &mut Self {
483        let line = cookie.encode();
484        self.out.header("set-cookie", line.as_bytes());
485        self
486    }
487
488    /// The conditional fast path for a page: sets `ETag: "<tag>"` and, when
489    /// the request's `If-None-Match` matches it (a `GET` or `HEAD`), turns the
490    /// reply into `304 Not Modified` and returns `true`, so the handler
491    /// returns before building the page.
492    ///
493    /// `tag` is the opaque part (letters, digits, and `-._~` are safe), for
494    /// example a revision number. An unusable tag sets nothing and returns
495    /// `false`.
496    ///
497    /// ```
498    /// use web_net::{App, get};
499    ///
500    /// let app = App::builder()
501    ///     .route(get("/", |cx| {
502    ///         if cx.not_modified("rev-7") {
503    ///             return Ok(());
504    ///         }
505    ///         cx.html(200, "<h1>page</h1>");
506    ///         Ok(())
507    ///     }).public())
508    ///     .build()?;
509    /// let r = serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: a\r\nIf-None-Match: \"rev-7\"\r\n\r\n");
510    /// assert_eq!(r.status, 304);
511    /// # Ok::<(), web_net::BuildError>(())
512    /// ```
513    pub fn not_modified(&mut self, tag: &str) -> bool {
514        let Ok(current) = parse_net::EntityTag::strong(tag.as_bytes()) else {
515            return false;
516        };
517        let quoted = format!("\"{tag}\"");
518        self.out.header("etag", quoted.as_bytes());
519        if !matches!(self.method(), Method::Get | Method::Head) {
520            return false;
521        }
522        let matched = self
523            .req
524            .header("if-none-match")
525            .is_some_and(|v| parse_net::if_none_match(v, &current).unwrap_or(false));
526        if matched {
527            self.out.status = 304;
528            self.out.body.clear();
529        }
530        matched
531    }
532
533    fn content_type(&mut self, value: &str) {
534        if !self.out.has_type {
535            self.out.header("content-type", value.as_bytes());
536        }
537    }
538
539    /// The site-relative `path` as a URL path: the site's base path in
540    /// front (`/blog` + `/posts/1`). Every link a page writes goes through
541    /// this, so the site works installed in a folder.
542    #[must_use]
543    pub fn url(&self, path: &str) -> String {
544        let base = self.shared.base.as_ref();
545        if path == "/" && !base.is_empty() {
546            format!("{base}/")
547        } else {
548            format!("{base}{path}")
549        }
550    }
551
552    /// `303 See Other` to a site-relative path: the answer to a form that
553    /// was accepted, so a reload does not post it again.
554    ///
555    /// # Errors
556    ///
557    /// 500 `redirect.not_local` when `path` is not a local path (it must
558    /// start with one `/`): redirects to other hosts are never built from a
559    /// string a handler passes along.
560    pub fn see_other(&mut self, path: &str) -> Result {
561        self.redirect(303, path)
562    }
563
564    /// A redirect to another site, only to a host listed with
565    /// [`Builder::redirect_hosts`](crate::Builder::redirect_hosts):
566    /// `https://` (or `http://`), the host, and a visible-ASCII rest.
567    ///
568    /// # Errors
569    ///
570    /// 500 `redirect.not_allowed` for any other target, 500
571    /// `redirect.status` for a status that is not a redirect.
572    pub fn redirect_external(&mut self, status: u16, url: &str) -> Result {
573        if !matches!(status, 301 | 302 | 303 | 307 | 308) {
574            return Err(Error::internal("redirect.status"));
575        }
576        let rest = url
577            .strip_prefix("https://")
578            .or_else(|| url.strip_prefix("http://"))
579            .ok_or_else(|| Error::internal("redirect.not_allowed"))?;
580        let end = rest.find(['/', '?', '#']).unwrap_or(rest.len());
581        let authority = rest[..end].to_ascii_lowercase();
582        let host = authority
583            .rsplit_once(':')
584            .map_or(authority.as_str(), |(h, p)| {
585                if p.bytes().all(|b| b.is_ascii_digit()) && !p.is_empty() {
586                    h
587                } else {
588                    ""
589                }
590            });
591        let listed = self
592            .shared
593            .redirect_hosts
594            .iter()
595            .any(|h| h.as_ref() == authority || h.as_ref() == host);
596        let visible = url.bytes().all(|c| (0x21..0x7f).contains(&c) && c != b'\\');
597        if !listed || !visible || authority.contains('@') {
598            return Err(Error::internal("redirect.not_allowed"));
599        }
600        self.out.header("location", url.as_bytes());
601        self.out.status = status;
602        self.out.body.clear();
603        Ok(())
604    }
605
606    /// A redirect with `status` (301, 302, 303, 307, 308) to a
607    /// site-relative path.
608    ///
609    /// # Errors
610    ///
611    /// As [`see_other`](Self::see_other); 500 `redirect.status` for a
612    /// status that is not a redirect.
613    pub fn redirect(&mut self, status: u16, path: &str) -> Result {
614        if !matches!(status, 301 | 302 | 303 | 307 | 308) {
615            return Err(Error::internal("redirect.status"));
616        }
617        if !is_local_target(path) {
618            return Err(Error::internal("redirect.not_local"));
619        }
620        let location = self.url(path);
621        self.out.header("location", location.as_bytes());
622        self.out.status = status;
623        self.out.body.clear();
624        Ok(())
625    }
626
627    /// Renders the route's template (declared with
628    /// [`Route::template`](crate::Route::template)) against `view` into the
629    /// body. The status stays as set (200 by default); `content-type` is
630    /// HTML unless the handler set one.
631    ///
632    /// # Errors
633    ///
634    /// 500 `render.no_engine` or `render.no_template` when nothing can
635    /// render; the engine's own [`RenderError`](crate::RenderError) as a 500;
636    /// a checkpoint's denial.
637    pub fn render(&mut self, view: &dyn View) -> Result {
638        let t = self
639            .template
640            .ok_or_else(|| Error::internal("render.no_template"))?;
641        self.render_id(t, view)
642    }
643
644    /// Renders the template `name` against `view`. The name is resolved by
645    /// the engine on every call; prefer [`render`](Self::render) with a
646    /// template declared on the route, resolved once at build.
647    ///
648    /// # Errors
649    ///
650    /// As [`render`](Self::render).
651    pub fn render_named(&mut self, name: &str, view: &dyn View) -> Result {
652        let engine = self
653            .shared
654            .render
655            .as_deref()
656            .ok_or_else(|| Error::internal("render.no_engine"))?;
657        let t = engine
658            .template(name)
659            .ok_or_else(|| Error::internal("render.no_template"))?;
660        self.render_id(t, view)
661    }
662
663    fn render_id(&mut self, t: TemplateId, view: &dyn View) -> Result {
664        let engine = self
665            .shared
666            .render
667            .as_deref()
668            .ok_or_else(|| Error::internal("render.no_engine"))?;
669        if !self.rendering {
670            // Hooks may render too; they do not run their own hooks again.
671            self.rendering = true;
672            let hooks = self.before_render;
673            let ran = hooks.iter().try_for_each(|h| h(self));
674            self.rendering = false;
675            ran?;
676        }
677        if let Some(cp) = &self.shared.checkpoint {
678            match cp.check(self.req.env, Stage::BeforeRender) {
679                Verdict::Pass => {}
680                Verdict::Deny { status } => return Err(Error::new(status, "checkpoint.denied")),
681                _ => return Err(Error::forbidden("checkpoint.killed")),
682            }
683        }
684        self.out.body.clear();
685        let mut output = Output::new(&mut self.out.body, self.shared.page_limit);
686        let rendered = engine.render(t, view, &mut output);
687        if let Err(e) = rendered {
688            self.out.body.clear();
689            return Err(e.into());
690        }
691        self.content_type("text/html; charset=utf-8");
692        Ok(())
693    }
694}
695
696impl<I> fmt::Debug for Cx<'_, I> {
697    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
698        f.debug_struct("Cx")
699            .field("request", &self.req)
700            .field("status", &self.out.status)
701            .finish_non_exhaustive()
702    }
703}
704
705/// Decodes one query name or value: `+` is a space, escapes decoded once.
706fn decode_query(s: &str) -> Option<Cow<'_, str>> {
707    if !s.contains(['%', '+']) {
708        return Some(Cow::Borrowed(s));
709    }
710    let b = s.as_bytes();
711    let mut out = Vec::with_capacity(b.len());
712    let mut i = 0;
713    while i < b.len() {
714        match b[i] {
715            b'+' => {
716                out.push(b' ');
717                i += 1;
718            }
719            b'%' => {
720                let h = b.get(i + 1).and_then(|&c| (c as char).to_digit(16))?;
721                let l = b.get(i + 2).and_then(|&c| (c as char).to_digit(16))?;
722                out.push(((h << 4) | l) as u8);
723                i += 3;
724            }
725            c => {
726                out.push(c);
727                i += 1;
728            }
729        }
730    }
731    String::from_utf8(out).ok().map(Cow::Owned)
732}
733
734#[cfg(test)]
735mod tests {
736    use super::*;
737
738    #[test]
739    fn test_local_targets() {
740        for ok in ["/", "/a", "/a/b?c=d", "/a%20b"] {
741            assert!(is_local_target(ok), "{ok}");
742        }
743        for bad in [
744            "",
745            "a",
746            "//evil.example",
747            "/\\evil.example",
748            "https://x",
749            "/a\r\nx",
750            "/a b",
751            "/a\\b",
752        ] {
753            assert!(!is_local_target(bad), "{bad:?}");
754        }
755    }
756
757    #[test]
758    fn test_decode_query() {
759        assert_eq!(decode_query("a+b%21").as_deref(), Some("a b!"));
760        assert_eq!(decode_query("plain").as_deref(), Some("plain"));
761        assert_eq!(decode_query("%zz"), None);
762        assert_eq!(decode_query("%ff"), None);
763    }
764
765    #[test]
766    fn test_out_refuses_bad_fields() {
767        let mut o = Out::default();
768        o.header("x-ok", b"v");
769        assert!(!o.invalid);
770        o.header("content-length", b"1");
771        assert!(o.invalid);
772        let mut o = Out::default();
773        o.header("x", b"a\r\nb");
774        assert!(o.invalid);
775        let mut o = Out::default();
776        o.header("bad name", b"v");
777        assert!(o.invalid);
778        let mut o = Out::default();
779        o.header("Content-Type", b"text/x");
780        assert!(o.has_type && o.has("content-type"));
781    }
782}