1use 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#[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
29const 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 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
93pub(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
103pub 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 #[must_use]
126 pub fn request(&self) -> &Request<'r> {
127 &self.req
128 }
129
130 #[must_use]
132 pub fn method(&self) -> Method {
133 self.req.method()
134 }
135
136 #[must_use]
138 pub fn path(&self) -> &'r str {
139 self.req.path
140 }
141
142 #[must_use]
144 pub fn header(&self, name: &str) -> Option<&'r [u8]> {
145 self.req.header(name)
146 }
147
148 #[must_use]
150 pub fn identity(&self) -> &'r I {
151 self.who
152 }
153
154 #[must_use]
156 pub fn id(&self) -> RequestId {
157 self.req.id()
158 }
159
160 #[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 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 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 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 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 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 #[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 #[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 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 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 pub fn status(&mut self, status: u16) -> &mut Self {
422 self.out.status = status;
423 self
424 }
425
426 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 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 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 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 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 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, ¤t).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 #[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 pub fn see_other(&mut self, path: &str) -> Result {
549 self.redirect(303, path)
550 }
551
552 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 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 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 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
685fn 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}