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