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::{NoRender, Output, Render, 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 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 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 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
119pub(crate) fn is_local_target(p: &str) -> bool {
123 let b = p.as_bytes();
124 b.first() == Some(&b'/')
125 && !matches!(b.get(1), Some(b'/' | b'\\'))
126 && b.iter().all(|&c| (0x21..0x7f).contains(&c) && c != b'\\')
127}
128
129pub struct Cx<'r, I, E: Render = NoRender> {
135 pub(crate) req: Request<'r>,
136 pub(crate) who: &'r I,
137 pub(crate) caps: Captures,
138 pub(crate) names: &'r [Box<str>],
139 pub(crate) out: &'r mut Out,
140 pub(crate) shared: &'r Shared<E>,
141 pub(crate) template: Option<TemplateId>,
142 pub(crate) binding: Option<&'r [u8]>,
143 pub(crate) limits: &'r form_net::Limits,
144 pub(crate) before_render: &'r [crate::app::HookFn<I, E>],
145 pub(crate) rendering: bool,
146}
147
148impl<'r, I, E: Render> Cx<'r, I, E> {
149 #[must_use]
153 pub fn request(&self) -> &Request<'r> {
154 &self.req
155 }
156
157 #[must_use]
159 pub fn method(&self) -> Method {
160 self.req.method()
161 }
162
163 #[must_use]
165 pub fn path(&self) -> &'r str {
166 self.req.path
167 }
168
169 #[must_use]
171 pub fn header(&self, name: &str) -> Option<&'r [u8]> {
172 self.req.header(name)
173 }
174
175 #[must_use]
177 pub fn identity(&self) -> &'r I {
178 self.who
179 }
180
181 #[must_use]
183 pub fn id(&self) -> RequestId {
184 self.req.id()
185 }
186
187 #[must_use]
189 pub fn param(&self, name: &str) -> Option<Cow<'r, str>> {
190 let i = self.names.iter().position(|n| n.as_ref() == name)?;
191 let (s, e) = self.caps.get(i)?;
192 Some(url::decode(&self.req.path[s..e]))
193 }
194
195 pub fn param_as<T: FromStr>(&self, name: &str) -> Result<T> {
202 self.param(name)
203 .and_then(|v| v.parse().ok())
204 .ok_or_else(|| Error::not_found("route.param"))
205 }
206
207 pub fn query(&self, name: &str) -> Result<Option<Cow<'r, str>>> {
215 let Some(q) = self.req.query() else {
216 return Ok(None);
217 };
218 let mut found = None;
219 for pair in q.split('&') {
220 let (k, v) = pair.split_once('=').unwrap_or((pair, ""));
221 if decode_query(k).as_deref() != Some(name) {
222 continue;
223 }
224 if found.is_some() {
225 return Err(Error::bad_request("query.duplicate"));
226 }
227 found = Some(decode_query(v).ok_or_else(|| Error::bad_request("query.invalid"))?);
228 }
229 Ok(found)
230 }
231
232 pub fn body(&self) -> Result<&'r [u8]> {
240 Ok(self.req.http.body_bytes())
241 }
242
243 pub fn form<T: form_net::FromForm>(&mut self) -> std::result::Result<T, form_net::FormError> {
280 self.form_inspected(&mut form_net::NoInspector)
281 }
282
283 pub fn form_inspected<T: form_net::FromForm, N: form_net::Inspector>(
290 &mut self,
291 inspector: &mut N,
292 ) -> std::result::Result<T, form_net::FormError> {
293 let content_type = self.req.header("content-type").unwrap_or(b"");
294 let body = self.req.http.body_bytes();
295 T::from_body_inspected(
296 content_type,
297 body,
298 self.limits,
299 &form_net::Policy::DEFAULT,
300 inspector,
301 )
302 }
303
304 #[cfg(feature = "uploads")]
314 #[cfg_attr(docsrs, doc(cfg(feature = "uploads")))]
315 pub fn upload(
316 &mut self,
317 store: &form_net::Store,
318 ) -> std::result::Result<form_net::Upload, form_net::FormError> {
319 self.upload_inspected(store, &mut form_net::NoInspector)
320 }
321
322 #[cfg(feature = "uploads")]
328 #[cfg_attr(docsrs, doc(cfg(feature = "uploads")))]
329 pub fn upload_inspected<N: form_net::Inspector>(
330 &mut self,
331 store: &form_net::Store,
332 inspector: &mut N,
333 ) -> std::result::Result<form_net::Upload, form_net::FormError> {
334 let content_type = self.req.header("content-type").unwrap_or(b"");
335 let body = self.req.http.body_bytes();
336 let mut session = form_net::UploadSession::new(
337 content_type,
338 self.limits,
339 &form_net::Policy::DEFAULT,
340 store,
341 )?;
342 session.push(body, inspector)?;
343 session.finish(inspector)
344 }
345
346 pub fn csrf_token(&self) -> Result<String> {
355 let engine = self
356 .shared
357 .csrf
358 .as_ref()
359 .ok_or_else(|| Error::internal("csrf.no_engine"))?;
360 let binding = self
361 .binding
362 .filter(|b| !b.is_empty())
363 .ok_or_else(|| Error::internal("csrf.no_binding"))?;
364 engine
365 .issue(binding)
366 .map(|t| t.as_str().to_owned())
367 .map_err(|e| Error::internal("csrf.issue").with_source(e))
368 }
369
370 pub fn precondition(
380 &self,
381 tag: Option<&str>,
382 modified: Option<std::time::SystemTime>,
383 ) -> Result {
384 let current = tag.and_then(|t| parse_net::EntityTag::strong(t.as_bytes()).ok());
385 if let Some(v) = self.req.header("if-match") {
386 if !parse_net::if_match(v, current.as_ref()).unwrap_or(false) {
387 return Err(Error::new(412, "precondition.failed"));
388 }
389 return Ok(());
390 }
391 if let (Some(v), Some(m)) = (self.req.header("if-unmodified-since"), modified) {
392 let (Ok(since), Ok(lm)) = (
393 parse_net::http_date(v),
394 parse_net::HttpDate::from_system_time(m),
395 ) else {
396 return Ok(());
397 };
398 if lm > since {
399 return Err(Error::new(412, "precondition.failed"));
400 }
401 }
402 Ok(())
403 }
404
405 pub fn status(&mut self, status: u16) -> &mut Self {
409 self.out.status = status;
410 self
411 }
412
413 pub fn set_header(&mut self, name: &str, value: impl AsRef<[u8]>) -> &mut Self {
417 self.out.header(name, value.as_ref());
418 self
419 }
420
421 pub fn body_bytes(&mut self, bytes: &[u8]) -> &mut Self {
423 self.out.body.clear();
424 self.out.body.extend_from_slice(bytes);
425 self
426 }
427
428 #[must_use]
432 pub fn language(&self, offers: &[&'static str]) -> Option<&'static str> {
433 let i = parse_net::best_language(self.req.header("accept-language"), offers)?;
434 offers.get(i).copied()
435 }
436
437 #[must_use]
440 pub fn media(&self, offers: &[&'static str]) -> Option<&'static str> {
441 let i = parse_net::best_media(self.req.header("accept"), offers)?;
442 offers.get(i).copied()
443 }
444
445 pub fn body_mut(&mut self) -> &mut Vec<u8> {
449 self.out.body.clear();
450 &mut self.out.body
451 }
452
453 pub fn html(&mut self, status: u16, html: &str) -> &mut Self {
455 self.content_type("text/html; charset=utf-8");
456 self.status(status).body_bytes(html.as_bytes())
457 }
458
459 pub fn text(&mut self, status: u16, text: &str) -> &mut Self {
461 self.content_type("text/plain; charset=utf-8");
462 self.status(status).body_bytes(text.as_bytes())
463 }
464
465 pub fn set_cookie(&mut self, cookie: &cookie_net::SetCookie<'_>) -> &mut Self {
483 let line = cookie.encode();
484 self.out.header("set-cookie", line.as_bytes());
485 self
486 }
487
488 pub fn not_modified(&mut self, tag: &str) -> bool {
514 let Ok(current) = parse_net::EntityTag::strong(tag.as_bytes()) else {
515 return false;
516 };
517 let quoted = format!("\"{tag}\"");
518 self.out.header("etag", quoted.as_bytes());
519 if !matches!(self.method(), Method::Get | Method::Head) {
520 return false;
521 }
522 let matched = self
523 .req
524 .header("if-none-match")
525 .is_some_and(|v| parse_net::if_none_match(v, ¤t).unwrap_or(false));
526 if matched {
527 self.out.status = 304;
528 self.out.body.clear();
529 }
530 matched
531 }
532
533 fn content_type(&mut self, value: &str) {
534 if !self.out.has_type {
535 self.out.header("content-type", value.as_bytes());
536 }
537 }
538
539 #[must_use]
543 pub fn url(&self, path: &str) -> String {
544 let base = self.shared.base.as_ref();
545 if path == "/" && !base.is_empty() {
546 format!("{base}/")
547 } else {
548 format!("{base}{path}")
549 }
550 }
551
552 pub fn see_other(&mut self, path: &str) -> Result {
561 self.redirect(303, path)
562 }
563
564 pub fn next_path(&self) -> Result<Option<String>> {
594 let Some(next) = self.query("next")? else {
595 return Ok(None);
596 };
597 let base = self.shared.base.as_ref();
598 let rel = if base.is_empty() {
599 next.as_ref()
600 } else {
601 match next.strip_prefix(base) {
602 Some("") => "/",
603 Some(r) if r.starts_with('/') => r,
604 _ => return Ok(None),
605 }
606 };
607 Ok(is_local_target(rel).then(|| rel.to_owned()))
608 }
609
610 pub fn redirect_external(&mut self, status: u16, url: &str) -> Result {
619 if !matches!(status, 301 | 302 | 303 | 307 | 308) {
620 return Err(Error::internal("redirect.status"));
621 }
622 let rest = url
623 .strip_prefix("https://")
624 .or_else(|| url.strip_prefix("http://"))
625 .ok_or_else(|| Error::internal("redirect.not_allowed"))?;
626 let end = rest.find(['/', '?', '#']).unwrap_or(rest.len());
627 let authority = rest[..end].to_ascii_lowercase();
628 let host = authority
629 .rsplit_once(':')
630 .map_or(authority.as_str(), |(h, p)| {
631 if p.bytes().all(|b| b.is_ascii_digit()) && !p.is_empty() {
632 h
633 } else {
634 ""
635 }
636 });
637 let listed = self
638 .shared
639 .redirect_hosts
640 .iter()
641 .any(|h| h.as_ref() == authority || h.as_ref() == host);
642 let visible = url.bytes().all(|c| (0x21..0x7f).contains(&c) && c != b'\\');
643 if !listed || !visible || authority.contains('@') {
644 return Err(Error::internal("redirect.not_allowed"));
645 }
646 self.out.header("location", url.as_bytes());
647 self.out.status = status;
648 self.out.body.clear();
649 Ok(())
650 }
651
652 pub fn redirect(&mut self, status: u16, path: &str) -> Result {
660 if !matches!(status, 301 | 302 | 303 | 307 | 308) {
661 return Err(Error::internal("redirect.status"));
662 }
663 if !is_local_target(path) {
664 return Err(Error::internal("redirect.not_local"));
665 }
666 let location = self.url(path);
667 self.out.header("location", location.as_bytes());
668 self.out.status = status;
669 self.out.body.clear();
670 Ok(())
671 }
672
673 pub fn render(&mut self, view: &E::View<'_>) -> Result {
684 let t = self
685 .template
686 .ok_or_else(|| Error::internal("render.no_template"))?;
687 self.render_id(t, view)
688 }
689
690 pub fn render_named(&mut self, name: &str, view: &E::View<'_>) -> Result {
698 let engine = self
699 .shared
700 .render
701 .as_ref()
702 .ok_or_else(|| Error::internal("render.no_engine"))?;
703 let t = engine
704 .template(name)
705 .ok_or_else(|| Error::internal("render.no_template"))?;
706 self.render_id(t, view)
707 }
708
709 fn render_id(&mut self, t: TemplateId, view: &E::View<'_>) -> Result {
710 let engine = self
711 .shared
712 .render
713 .as_ref()
714 .ok_or_else(|| Error::internal("render.no_engine"))?;
715 if !self.rendering {
716 self.rendering = true;
718 let hooks = self.before_render;
719 let ran = hooks.iter().try_for_each(|h| h(self));
720 self.rendering = false;
721 ran?;
722 }
723 if let Some(cp) = &self.shared.checkpoint {
724 match cp.check(self.req.env, Stage::BeforeRender) {
725 Verdict::Pass => {}
726 Verdict::Deny { status } => return Err(Error::new(status, "checkpoint.denied")),
727 _ => return Err(Error::forbidden("checkpoint.killed")),
728 }
729 }
730 self.out.body.clear();
731 let mut output = Output::new(&mut self.out.body, self.shared.page_limit);
732 let rendered = engine.render(t, view, &mut output);
733 let over = output.over();
734 if let Err(e) = rendered {
735 self.out.body.clear();
736 return Err(e.into());
737 }
738 if over {
739 self.out.body.clear();
740 return Err(crate::RenderError::new("render.too_large").into());
741 }
742 self.content_type("text/html; charset=utf-8");
743 Ok(())
744 }
745}
746
747impl<I, E> Cx<'_, I, E>
748where
749 E: for<'a> Render<View<'a> = dyn View + 'a>,
750{
751 pub fn render_view(&mut self, view: &dyn View) -> Result {
759 self.render(view)
760 }
761
762 pub fn render_view_named(&mut self, name: &str, view: &dyn View) -> Result {
769 self.render_named(name, view)
770 }
771}
772
773impl<I, E: Render> fmt::Debug for Cx<'_, I, E> {
774 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
775 f.debug_struct("Cx")
776 .field("request", &self.req)
777 .field("status", &self.out.status)
778 .finish_non_exhaustive()
779 }
780}
781
782fn decode_query(s: &str) -> Option<Cow<'_, str>> {
784 if !s.contains(['%', '+']) {
785 return Some(Cow::Borrowed(s));
786 }
787 let b = s.as_bytes();
788 let mut out = Vec::with_capacity(b.len());
789 let mut i = 0;
790 while i < b.len() {
791 match b[i] {
792 b'+' => {
793 out.push(b' ');
794 i += 1;
795 }
796 b'%' => {
797 let h = b.get(i + 1).and_then(|&c| (c as char).to_digit(16))?;
798 let l = b.get(i + 2).and_then(|&c| (c as char).to_digit(16))?;
799 out.push(((h << 4) | l) as u8);
800 i += 3;
801 }
802 c => {
803 out.push(c);
804 i += 1;
805 }
806 }
807 }
808 String::from_utf8(out).ok().map(Cow::Owned)
809}
810
811#[cfg(test)]
812mod tests {
813 use super::*;
814
815 #[test]
816 fn test_local_targets() {
817 for ok in ["/", "/a", "/a/b?c=d", "/a%20b"] {
818 assert!(is_local_target(ok), "{ok}");
819 }
820 for bad in [
821 "",
822 "a",
823 "//evil.example",
824 "/\\evil.example",
825 "https://x",
826 "/a\r\nx",
827 "/a b",
828 "/a\\b",
829 ] {
830 assert!(!is_local_target(bad), "{bad:?}");
831 }
832 }
833
834 #[test]
835 fn test_decode_query() {
836 assert_eq!(decode_query("a+b%21").as_deref(), Some("a b!"));
837 assert_eq!(decode_query("plain").as_deref(), Some("plain"));
838 assert_eq!(decode_query("%zz"), None);
839 assert_eq!(decode_query("%ff"), None);
840 }
841
842 #[test]
843 fn test_out_refuses_bad_fields() {
844 let mut o = Out::default();
845 o.header("x-ok", b"v");
846 assert!(!o.invalid);
847 o.header("content-length", b"1");
848 assert!(o.invalid);
849 let mut o = Out::default();
850 o.header("x", b"a\r\nb");
851 assert!(o.invalid);
852 let mut o = Out::default();
853 o.header("bad name", b"v");
854 assert!(o.invalid);
855 let mut o = Out::default();
856 o.header("Content-Type", b"text/x");
857 assert!(o.has_type && o.has("content-type"));
858 }
859}