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}
117
118impl<'r, I> Cx<'r, I> {
119 #[must_use]
123 pub fn request(&self) -> &Request<'r> {
124 &self.req
125 }
126
127 #[must_use]
129 pub fn method(&self) -> Method {
130 self.req.method()
131 }
132
133 #[must_use]
135 pub fn path(&self) -> &'r str {
136 self.req.path
137 }
138
139 #[must_use]
141 pub fn header(&self, name: &str) -> Option<&'r [u8]> {
142 self.req.header(name)
143 }
144
145 #[must_use]
147 pub fn identity(&self) -> &'r I {
148 self.who
149 }
150
151 #[must_use]
153 pub fn id(&self) -> RequestId {
154 self.req.id()
155 }
156
157 #[must_use]
159 pub fn param(&self, name: &str) -> Option<Cow<'r, str>> {
160 let i = self.names.iter().position(|n| n.as_ref() == name)?;
161 let (s, e) = self.caps.get(i)?;
162 Some(url::decode(&self.req.path[s..e]))
163 }
164
165 pub fn param_as<T: FromStr>(&self, name: &str) -> Result<T> {
172 self.param(name)
173 .and_then(|v| v.parse().ok())
174 .ok_or_else(|| Error::not_found("route.param"))
175 }
176
177 pub fn query(&self, name: &str) -> Result<Option<Cow<'r, str>>> {
185 let Some(q) = self.req.query() else {
186 return Ok(None);
187 };
188 let mut found = None;
189 for pair in q.split('&') {
190 let (k, v) = pair.split_once('=').unwrap_or((pair, ""));
191 if decode_query(k).as_deref() != Some(name) {
192 continue;
193 }
194 if found.is_some() {
195 return Err(Error::bad_request("query.duplicate"));
196 }
197 found = Some(decode_query(v).ok_or_else(|| Error::bad_request("query.invalid"))?);
198 }
199 Ok(found)
200 }
201
202 pub fn body(&self) -> Result<&'r [u8]> {
209 Ok(self.req.http.body_bytes())
210 }
211
212 pub fn status(&mut self, status: u16) -> &mut Self {
216 self.out.status = status;
217 self
218 }
219
220 pub fn set_header(&mut self, name: &str, value: impl AsRef<[u8]>) -> &mut Self {
224 self.out.header(name, value.as_ref());
225 self
226 }
227
228 pub fn body_bytes(&mut self, bytes: &[u8]) -> &mut Self {
230 self.out.body.clear();
231 self.out.body.extend_from_slice(bytes);
232 self
233 }
234
235 pub fn html(&mut self, status: u16, html: &str) -> &mut Self {
237 self.content_type("text/html; charset=utf-8");
238 self.status(status).body_bytes(html.as_bytes())
239 }
240
241 pub fn text(&mut self, status: u16, text: &str) -> &mut Self {
243 self.content_type("text/plain; charset=utf-8");
244 self.status(status).body_bytes(text.as_bytes())
245 }
246
247 pub fn set_cookie(&mut self, cookie: &cookie_net::SetCookie<'_>) -> &mut Self {
265 let line = cookie.encode();
266 self.out.header("set-cookie", line.as_bytes());
267 self
268 }
269
270 pub fn not_modified(&mut self, tag: &str) -> bool {
296 let Ok(current) = parse_net::EntityTag::strong(tag.as_bytes()) else {
297 return false;
298 };
299 let quoted = format!("\"{tag}\"");
300 self.out.header("etag", quoted.as_bytes());
301 if !matches!(self.method(), Method::Get | Method::Head) {
302 return false;
303 }
304 let matched = self
305 .req
306 .header("if-none-match")
307 .is_some_and(|v| parse_net::if_none_match(v, ¤t).unwrap_or(false));
308 if matched {
309 self.out.status = 304;
310 self.out.body.clear();
311 }
312 matched
313 }
314
315 fn content_type(&mut self, value: &str) {
316 if !self.out.has_type {
317 self.out.header("content-type", value.as_bytes());
318 }
319 }
320
321 #[must_use]
325 pub fn url(&self, path: &str) -> String {
326 let base = self.shared.base.as_ref();
327 if path == "/" && !base.is_empty() {
328 format!("{base}/")
329 } else {
330 format!("{base}{path}")
331 }
332 }
333
334 pub fn see_other(&mut self, path: &str) -> Result {
343 self.redirect(303, path)
344 }
345
346 pub fn redirect(&mut self, status: u16, path: &str) -> Result {
354 if !matches!(status, 301 | 302 | 303 | 307 | 308) {
355 return Err(Error::internal("redirect.status"));
356 }
357 if !is_local_target(path) {
358 return Err(Error::internal("redirect.not_local"));
359 }
360 let location = self.url(path);
361 self.out.header("location", location.as_bytes());
362 self.out.status = status;
363 self.out.body.clear();
364 Ok(())
365 }
366
367 pub fn render(&mut self, view: &dyn View) -> Result {
378 let t = self
379 .template
380 .ok_or_else(|| Error::internal("render.no_template"))?;
381 self.render_id(t, view)
382 }
383
384 pub fn render_named(&mut self, name: &str, view: &dyn View) -> Result {
392 let engine = self
393 .shared
394 .render
395 .as_deref()
396 .ok_or_else(|| Error::internal("render.no_engine"))?;
397 let t = engine
398 .template(name)
399 .ok_or_else(|| Error::internal("render.no_template"))?;
400 self.render_id(t, view)
401 }
402
403 fn render_id(&mut self, t: TemplateId, view: &dyn View) -> Result {
404 let engine = self
405 .shared
406 .render
407 .as_deref()
408 .ok_or_else(|| Error::internal("render.no_engine"))?;
409 if let Some(cp) = &self.shared.checkpoint {
410 match cp.check(self.req.env, Stage::BeforeRender) {
411 Verdict::Pass => {}
412 Verdict::Deny { status } => return Err(Error::new(status, "checkpoint.denied")),
413 _ => return Err(Error::forbidden("checkpoint.killed")),
414 }
415 }
416 self.out.body.clear();
417 let mut output = Output::new(&mut self.out.body, self.shared.page_limit);
418 let rendered = engine.render(t, view, &mut output);
419 if let Err(e) = rendered {
420 self.out.body.clear();
421 return Err(e.into());
422 }
423 self.content_type("text/html; charset=utf-8");
424 Ok(())
425 }
426}
427
428impl<I> fmt::Debug for Cx<'_, I> {
429 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
430 f.debug_struct("Cx")
431 .field("request", &self.req)
432 .field("status", &self.out.status)
433 .finish_non_exhaustive()
434 }
435}
436
437fn decode_query(s: &str) -> Option<Cow<'_, str>> {
439 if !s.contains(['%', '+']) {
440 return Some(Cow::Borrowed(s));
441 }
442 let b = s.as_bytes();
443 let mut out = Vec::with_capacity(b.len());
444 let mut i = 0;
445 while i < b.len() {
446 match b[i] {
447 b'+' => {
448 out.push(b' ');
449 i += 1;
450 }
451 b'%' => {
452 let h = b.get(i + 1).and_then(|&c| (c as char).to_digit(16))?;
453 let l = b.get(i + 2).and_then(|&c| (c as char).to_digit(16))?;
454 out.push(((h << 4) | l) as u8);
455 i += 3;
456 }
457 c => {
458 out.push(c);
459 i += 1;
460 }
461 }
462 }
463 String::from_utf8(out).ok().map(Cow::Owned)
464}
465
466#[cfg(test)]
467mod tests {
468 use super::*;
469
470 #[test]
471 fn test_local_targets() {
472 for ok in ["/", "/a", "/a/b?c=d", "/a%20b"] {
473 assert!(is_local_target(ok), "{ok}");
474 }
475 for bad in [
476 "",
477 "a",
478 "//evil.example",
479 "/\\evil.example",
480 "https://x",
481 "/a\r\nx",
482 "/a b",
483 "/a\\b",
484 ] {
485 assert!(!is_local_target(bad), "{bad:?}");
486 }
487 }
488
489 #[test]
490 fn test_decode_query() {
491 assert_eq!(decode_query("a+b%21").as_deref(), Some("a b!"));
492 assert_eq!(decode_query("plain").as_deref(), Some("plain"));
493 assert_eq!(decode_query("%zz"), None);
494 assert_eq!(decode_query("%ff"), None);
495 }
496
497 #[test]
498 fn test_out_refuses_bad_fields() {
499 let mut o = Out::default();
500 o.header("x-ok", b"v");
501 assert!(!o.invalid);
502 o.header("content-length", b"1");
503 assert!(o.invalid);
504 let mut o = Out::default();
505 o.header("x", b"a\r\nb");
506 assert!(o.invalid);
507 let mut o = Out::default();
508 o.header("bad name", b"v");
509 assert!(o.invalid);
510 let mut o = Out::default();
511 o.header("Content-Type", b"text/x");
512 assert!(o.has_type && o.has("content-type"));
513 }
514}