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, ¤t).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}