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 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 has(&self, name: &str) -> bool {
87 self.spans
88 .iter()
89 .any(|&(s, e, _, _)| &self.names[s as usize..e as usize] == name.as_bytes())
90 }
91}
92
93const fn is_tchar(b: u8) -> bool {
94 matches!(b,
95 b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9'
96 | b'!' | b'#' | b'$' | b'%' | b'&' | b'\'' | b'*' | b'+' | b'-' | b'.'
97 | b'^' | b'_' | b'`' | b'|' | b'~')
98}
99
100pub(crate) fn is_local_target(p: &str) -> bool {
104 let b = p.as_bytes();
105 b.first() == Some(&b'/')
106 && !matches!(b.get(1), Some(b'/' | b'\\'))
107 && b.iter().all(|&c| (0x21..0x7f).contains(&c) && c != b'\\')
108}
109
110pub struct Cx<'r, I> {
116 pub(crate) req: Request<'r>,
117 pub(crate) who: &'r I,
118 pub(crate) caps: Captures,
119 pub(crate) names: &'r [Box<str>],
120 pub(crate) out: &'r mut Out,
121 pub(crate) shared: &'r Shared,
122 pub(crate) template: Option<TemplateId>,
123 pub(crate) csrf: crate::csrf::State,
124 pub(crate) binding: Option<&'r [u8]>,
125 pub(crate) limits: &'r form_net::Limits,
126 pub(crate) before_render: &'r [crate::app::HookFn<I>],
127 pub(crate) rendering: bool,
128}
129
130impl<'r, I> Cx<'r, I> {
131 #[must_use]
135 pub fn request(&self) -> &Request<'r> {
136 &self.req
137 }
138
139 #[must_use]
141 pub fn method(&self) -> Method {
142 self.req.method()
143 }
144
145 #[must_use]
147 pub fn path(&self) -> &'r str {
148 self.req.path
149 }
150
151 #[must_use]
153 pub fn header(&self, name: &str) -> Option<&'r [u8]> {
154 self.req.header(name)
155 }
156
157 #[must_use]
159 pub fn identity(&self) -> &'r I {
160 self.who
161 }
162
163 #[must_use]
165 pub fn id(&self) -> RequestId {
166 self.req.id()
167 }
168
169 #[must_use]
171 pub fn param(&self, name: &str) -> Option<Cow<'r, str>> {
172 let i = self.names.iter().position(|n| n.as_ref() == name)?;
173 let (s, e) = self.caps.get(i)?;
174 Some(url::decode(&self.req.path[s..e]))
175 }
176
177 pub fn param_as<T: FromStr>(&self, name: &str) -> Result<T> {
184 self.param(name)
185 .and_then(|v| v.parse().ok())
186 .ok_or_else(|| Error::not_found("route.param"))
187 }
188
189 pub fn query(&self, name: &str) -> Result<Option<Cow<'r, str>>> {
197 let Some(q) = self.req.query() else {
198 return Ok(None);
199 };
200 let mut found = None;
201 for pair in q.split('&') {
202 let (k, v) = pair.split_once('=').unwrap_or((pair, ""));
203 if decode_query(k).as_deref() != Some(name) {
204 continue;
205 }
206 if found.is_some() {
207 return Err(Error::bad_request("query.duplicate"));
208 }
209 found = Some(decode_query(v).ok_or_else(|| Error::bad_request("query.invalid"))?);
210 }
211 Ok(found)
212 }
213
214 pub fn body(&self) -> Result<&'r [u8]> {
222 if self.csrf == crate::csrf::State::Pending {
223 return Err(Error::forbidden("csrf.unverified"));
224 }
225 Ok(self.req.http.body_bytes())
226 }
227
228 pub fn form<T: form_net::FromForm>(&mut self) -> std::result::Result<T, form_net::FormError> {
266 self.form_inspected(&mut form_net::NoInspector)
267 }
268
269 pub fn form_inspected<T: form_net::FromForm, N: form_net::Inspector>(
276 &mut self,
277 inspector: &mut N,
278 ) -> std::result::Result<T, form_net::FormError> {
279 let content_type = self.req.header("content-type").unwrap_or(b"");
280 let body = self.req.http.body_bytes();
281 match (self.csrf, self.shared.csrf.as_ref(), self.binding) {
282 (crate::csrf::State::Pending, Some(engine), Some(binding)) => {
283 let mut check = crate::csrf::token_check(engine, binding);
284 let result = T::from_body_inspected(
285 content_type,
286 body,
287 self.limits,
288 &crate::csrf::TOKEN_POLICY,
289 &mut (&mut check, inspector),
290 );
291 if check.verified() {
292 self.csrf = crate::csrf::State::Verified;
293 }
294 result
295 }
296 _ => T::from_body_inspected(
297 content_type,
298 body,
299 self.limits,
300 &form_net::Policy::DEFAULT,
301 inspector,
302 ),
303 }
304 }
305
306 #[cfg(feature = "uploads")]
316 #[cfg_attr(docsrs, doc(cfg(feature = "uploads")))]
317 pub fn upload(
318 &mut self,
319 store: &form_net::Store,
320 ) -> std::result::Result<form_net::Upload, form_net::FormError> {
321 self.upload_inspected(store, &mut form_net::NoInspector)
322 }
323
324 #[cfg(feature = "uploads")]
330 #[cfg_attr(docsrs, doc(cfg(feature = "uploads")))]
331 pub fn upload_inspected<N: form_net::Inspector>(
332 &mut self,
333 store: &form_net::Store,
334 inspector: &mut N,
335 ) -> std::result::Result<form_net::Upload, form_net::FormError> {
336 let content_type = self.req.header("content-type").unwrap_or(b"");
337 let body = self.req.http.body_bytes();
338 match (self.csrf, self.shared.csrf.as_ref(), self.binding) {
339 (crate::csrf::State::Pending, Some(engine), Some(binding)) => {
340 let mut check = crate::csrf::token_check(engine, binding);
341 let mut hooks = (&mut check, inspector);
342 let mut session = form_net::UploadSession::new(
343 content_type,
344 self.limits,
345 &crate::csrf::TOKEN_POLICY,
346 store,
347 )?;
348 session.push(body, &mut hooks)?;
349 let upload = session.finish(&mut hooks)?;
350 if hooks.0.verified() {
351 self.csrf = crate::csrf::State::Verified;
352 }
353 Ok(upload)
354 }
355 _ => {
356 let mut session = form_net::UploadSession::new(
357 content_type,
358 self.limits,
359 &form_net::Policy::DEFAULT,
360 store,
361 )?;
362 session.push(body, inspector)?;
363 session.finish(inspector)
364 }
365 }
366 }
367
368 pub fn csrf_token(&self) -> Result<String> {
377 let engine = self
378 .shared
379 .csrf
380 .as_ref()
381 .ok_or_else(|| Error::internal("csrf.no_engine"))?;
382 let binding = self
383 .binding
384 .filter(|b| !b.is_empty())
385 .ok_or_else(|| Error::internal("csrf.no_binding"))?;
386 engine
387 .issue(binding)
388 .map(|t| t.as_str().to_owned())
389 .map_err(|e| Error::internal("csrf.issue").with_source(e))
390 }
391
392 pub fn precondition(
402 &self,
403 tag: Option<&str>,
404 modified: Option<std::time::SystemTime>,
405 ) -> Result {
406 let current = tag.and_then(|t| parse_net::EntityTag::strong(t.as_bytes()).ok());
407 if let Some(v) = self.req.header("if-match") {
408 if !parse_net::if_match(v, current.as_ref()).unwrap_or(false) {
409 return Err(Error::new(412, "precondition.failed"));
410 }
411 return Ok(());
412 }
413 if let (Some(v), Some(m)) = (self.req.header("if-unmodified-since"), modified) {
414 let (Ok(since), Ok(lm)) = (
415 parse_net::http_date(v),
416 parse_net::HttpDate::from_system_time(m),
417 ) else {
418 return Ok(());
419 };
420 if lm > since {
421 return Err(Error::new(412, "precondition.failed"));
422 }
423 }
424 Ok(())
425 }
426
427 pub fn status(&mut self, status: u16) -> &mut Self {
431 self.out.status = status;
432 self
433 }
434
435 pub fn set_header(&mut self, name: &str, value: impl AsRef<[u8]>) -> &mut Self {
439 self.out.header(name, value.as_ref());
440 self
441 }
442
443 pub fn body_bytes(&mut self, bytes: &[u8]) -> &mut Self {
445 self.out.body.clear();
446 self.out.body.extend_from_slice(bytes);
447 self
448 }
449
450 #[must_use]
454 pub fn language(&self, offers: &[&'static str]) -> Option<&'static str> {
455 let i = parse_net::best_language(self.req.header("accept-language"), offers)?;
456 offers.get(i).copied()
457 }
458
459 #[must_use]
462 pub fn media(&self, offers: &[&'static str]) -> Option<&'static str> {
463 let i = parse_net::best_media(self.req.header("accept"), offers)?;
464 offers.get(i).copied()
465 }
466
467 pub fn html(&mut self, status: u16, html: &str) -> &mut Self {
469 self.content_type("text/html; charset=utf-8");
470 self.status(status).body_bytes(html.as_bytes())
471 }
472
473 pub fn text(&mut self, status: u16, text: &str) -> &mut Self {
475 self.content_type("text/plain; charset=utf-8");
476 self.status(status).body_bytes(text.as_bytes())
477 }
478
479 pub fn set_cookie(&mut self, cookie: &cookie_net::SetCookie<'_>) -> &mut Self {
497 let line = cookie.encode();
498 self.out.header("set-cookie", line.as_bytes());
499 self
500 }
501
502 pub fn not_modified(&mut self, tag: &str) -> bool {
528 let Ok(current) = parse_net::EntityTag::strong(tag.as_bytes()) else {
529 return false;
530 };
531 let quoted = format!("\"{tag}\"");
532 self.out.header("etag", quoted.as_bytes());
533 if !matches!(self.method(), Method::Get | Method::Head) {
534 return false;
535 }
536 let matched = self
537 .req
538 .header("if-none-match")
539 .is_some_and(|v| parse_net::if_none_match(v, ¤t).unwrap_or(false));
540 if matched {
541 self.out.status = 304;
542 self.out.body.clear();
543 }
544 matched
545 }
546
547 fn content_type(&mut self, value: &str) {
548 if !self.out.has_type {
549 self.out.header("content-type", value.as_bytes());
550 }
551 }
552
553 #[must_use]
557 pub fn url(&self, path: &str) -> String {
558 let base = self.shared.base.as_ref();
559 if path == "/" && !base.is_empty() {
560 format!("{base}/")
561 } else {
562 format!("{base}{path}")
563 }
564 }
565
566 pub fn see_other(&mut self, path: &str) -> Result {
575 self.redirect(303, path)
576 }
577
578 pub fn redirect_external(&mut self, status: u16, url: &str) -> Result {
587 if !matches!(status, 301 | 302 | 303 | 307 | 308) {
588 return Err(Error::internal("redirect.status"));
589 }
590 let rest = url
591 .strip_prefix("https://")
592 .or_else(|| url.strip_prefix("http://"))
593 .ok_or_else(|| Error::internal("redirect.not_allowed"))?;
594 let end = rest.find(['/', '?', '#']).unwrap_or(rest.len());
595 let authority = rest[..end].to_ascii_lowercase();
596 let host = authority
597 .rsplit_once(':')
598 .map_or(authority.as_str(), |(h, p)| {
599 if p.bytes().all(|b| b.is_ascii_digit()) && !p.is_empty() {
600 h
601 } else {
602 ""
603 }
604 });
605 let listed = self
606 .shared
607 .redirect_hosts
608 .iter()
609 .any(|h| h.as_ref() == authority || h.as_ref() == host);
610 let visible = url.bytes().all(|c| (0x21..0x7f).contains(&c) && c != b'\\');
611 if !listed || !visible || authority.contains('@') {
612 return Err(Error::internal("redirect.not_allowed"));
613 }
614 self.out.header("location", url.as_bytes());
615 self.out.status = status;
616 self.out.body.clear();
617 Ok(())
618 }
619
620 pub fn redirect(&mut self, status: u16, path: &str) -> Result {
628 if !matches!(status, 301 | 302 | 303 | 307 | 308) {
629 return Err(Error::internal("redirect.status"));
630 }
631 if !is_local_target(path) {
632 return Err(Error::internal("redirect.not_local"));
633 }
634 let location = self.url(path);
635 self.out.header("location", location.as_bytes());
636 self.out.status = status;
637 self.out.body.clear();
638 Ok(())
639 }
640
641 pub fn render(&mut self, view: &dyn View) -> Result {
652 let t = self
653 .template
654 .ok_or_else(|| Error::internal("render.no_template"))?;
655 self.render_id(t, view)
656 }
657
658 pub fn render_named(&mut self, name: &str, view: &dyn View) -> Result {
666 let engine = self
667 .shared
668 .render
669 .as_deref()
670 .ok_or_else(|| Error::internal("render.no_engine"))?;
671 let t = engine
672 .template(name)
673 .ok_or_else(|| Error::internal("render.no_template"))?;
674 self.render_id(t, view)
675 }
676
677 fn render_id(&mut self, t: TemplateId, view: &dyn View) -> Result {
678 let engine = self
679 .shared
680 .render
681 .as_deref()
682 .ok_or_else(|| Error::internal("render.no_engine"))?;
683 if !self.rendering {
684 self.rendering = true;
686 let hooks = self.before_render;
687 let ran = hooks.iter().try_for_each(|h| h(self));
688 self.rendering = false;
689 ran?;
690 }
691 if let Some(cp) = &self.shared.checkpoint {
692 match cp.check(self.req.env, Stage::BeforeRender) {
693 Verdict::Pass => {}
694 Verdict::Deny { status } => return Err(Error::new(status, "checkpoint.denied")),
695 _ => return Err(Error::forbidden("checkpoint.killed")),
696 }
697 }
698 self.out.body.clear();
699 let mut output = Output::new(&mut self.out.body, self.shared.page_limit);
700 let rendered = engine.render(t, view, &mut output);
701 if let Err(e) = rendered {
702 self.out.body.clear();
703 return Err(e.into());
704 }
705 self.content_type("text/html; charset=utf-8");
706 Ok(())
707 }
708}
709
710impl<I> fmt::Debug for Cx<'_, I> {
711 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
712 f.debug_struct("Cx")
713 .field("request", &self.req)
714 .field("status", &self.out.status)
715 .finish_non_exhaustive()
716 }
717}
718
719fn decode_query(s: &str) -> Option<Cow<'_, str>> {
721 if !s.contains(['%', '+']) {
722 return Some(Cow::Borrowed(s));
723 }
724 let b = s.as_bytes();
725 let mut out = Vec::with_capacity(b.len());
726 let mut i = 0;
727 while i < b.len() {
728 match b[i] {
729 b'+' => {
730 out.push(b' ');
731 i += 1;
732 }
733 b'%' => {
734 let h = b.get(i + 1).and_then(|&c| (c as char).to_digit(16))?;
735 let l = b.get(i + 2).and_then(|&c| (c as char).to_digit(16))?;
736 out.push(((h << 4) | l) as u8);
737 i += 3;
738 }
739 c => {
740 out.push(c);
741 i += 1;
742 }
743 }
744 }
745 String::from_utf8(out).ok().map(Cow::Owned)
746}
747
748#[cfg(test)]
749mod tests {
750 use super::*;
751
752 #[test]
753 fn test_local_targets() {
754 for ok in ["/", "/a", "/a/b?c=d", "/a%20b"] {
755 assert!(is_local_target(ok), "{ok}");
756 }
757 for bad in [
758 "",
759 "a",
760 "//evil.example",
761 "/\\evil.example",
762 "https://x",
763 "/a\r\nx",
764 "/a b",
765 "/a\\b",
766 ] {
767 assert!(!is_local_target(bad), "{bad:?}");
768 }
769 }
770
771 #[test]
772 fn test_decode_query() {
773 assert_eq!(decode_query("a+b%21").as_deref(), Some("a b!"));
774 assert_eq!(decode_query("plain").as_deref(), Some("plain"));
775 assert_eq!(decode_query("%zz"), None);
776 assert_eq!(decode_query("%ff"), None);
777 }
778
779 #[test]
780 fn test_out_refuses_bad_fields() {
781 let mut o = Out::default();
782 o.header("x-ok", b"v");
783 assert!(!o.invalid);
784 o.header("content-length", b"1");
785 assert!(o.invalid);
786 let mut o = Out::default();
787 o.header("x", b"a\r\nb");
788 assert!(o.invalid);
789 let mut o = Out::default();
790 o.header("bad name", b"v");
791 assert!(o.invalid);
792 let mut o = Out::default();
793 o.header("Content-Type", b"text/x");
794 assert!(o.has_type && o.has("content-type"));
795 }
796}