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 fn content_type(&mut self, value: &str) {
248 if !self.out.has_type {
249 self.out.header("content-type", value.as_bytes());
250 }
251 }
252
253 #[must_use]
257 pub fn url(&self, path: &str) -> String {
258 let base = self.shared.base.as_ref();
259 if path == "/" && !base.is_empty() {
260 format!("{base}/")
261 } else {
262 format!("{base}{path}")
263 }
264 }
265
266 pub fn see_other(&mut self, path: &str) -> Result {
275 self.redirect(303, path)
276 }
277
278 pub fn redirect(&mut self, status: u16, path: &str) -> Result {
286 if !matches!(status, 301 | 302 | 303 | 307 | 308) {
287 return Err(Error::internal("redirect.status"));
288 }
289 if !is_local_target(path) {
290 return Err(Error::internal("redirect.not_local"));
291 }
292 let location = self.url(path);
293 self.out.header("location", location.as_bytes());
294 self.out.status = status;
295 self.out.body.clear();
296 Ok(())
297 }
298
299 pub fn render(&mut self, view: &dyn View) -> Result {
310 let t = self
311 .template
312 .ok_or_else(|| Error::internal("render.no_template"))?;
313 self.render_id(t, view)
314 }
315
316 pub fn render_named(&mut self, name: &str, view: &dyn View) -> Result {
324 let engine = self
325 .shared
326 .render
327 .as_deref()
328 .ok_or_else(|| Error::internal("render.no_engine"))?;
329 let t = engine
330 .template(name)
331 .ok_or_else(|| Error::internal("render.no_template"))?;
332 self.render_id(t, view)
333 }
334
335 fn render_id(&mut self, t: TemplateId, view: &dyn View) -> Result {
336 let engine = self
337 .shared
338 .render
339 .as_deref()
340 .ok_or_else(|| Error::internal("render.no_engine"))?;
341 if let Some(cp) = &self.shared.checkpoint {
342 match cp.check(self.req.env, Stage::BeforeRender) {
343 Verdict::Pass => {}
344 Verdict::Deny { status } => return Err(Error::new(status, "checkpoint.denied")),
345 _ => return Err(Error::forbidden("checkpoint.killed")),
346 }
347 }
348 self.out.body.clear();
349 let mut output = Output::new(&mut self.out.body, self.shared.page_limit);
350 let rendered = engine.render(t, view, &mut output);
351 if let Err(e) = rendered {
352 self.out.body.clear();
353 return Err(e.into());
354 }
355 self.content_type("text/html; charset=utf-8");
356 Ok(())
357 }
358}
359
360impl<I> fmt::Debug for Cx<'_, I> {
361 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
362 f.debug_struct("Cx")
363 .field("request", &self.req)
364 .field("status", &self.out.status)
365 .finish_non_exhaustive()
366 }
367}
368
369fn decode_query(s: &str) -> Option<Cow<'_, str>> {
371 if !s.contains(['%', '+']) {
372 return Some(Cow::Borrowed(s));
373 }
374 let b = s.as_bytes();
375 let mut out = Vec::with_capacity(b.len());
376 let mut i = 0;
377 while i < b.len() {
378 match b[i] {
379 b'+' => {
380 out.push(b' ');
381 i += 1;
382 }
383 b'%' => {
384 let h = b.get(i + 1).and_then(|&c| (c as char).to_digit(16))?;
385 let l = b.get(i + 2).and_then(|&c| (c as char).to_digit(16))?;
386 out.push(((h << 4) | l) as u8);
387 i += 3;
388 }
389 c => {
390 out.push(c);
391 i += 1;
392 }
393 }
394 }
395 String::from_utf8(out).ok().map(Cow::Owned)
396}
397
398#[cfg(test)]
399mod tests {
400 use super::*;
401
402 #[test]
403 fn test_local_targets() {
404 for ok in ["/", "/a", "/a/b?c=d", "/a%20b"] {
405 assert!(is_local_target(ok), "{ok}");
406 }
407 for bad in [
408 "",
409 "a",
410 "//evil.example",
411 "/\\evil.example",
412 "https://x",
413 "/a\r\nx",
414 "/a b",
415 "/a\\b",
416 ] {
417 assert!(!is_local_target(bad), "{bad:?}");
418 }
419 }
420
421 #[test]
422 fn test_decode_query() {
423 assert_eq!(decode_query("a+b%21").as_deref(), Some("a b!"));
424 assert_eq!(decode_query("plain").as_deref(), Some("plain"));
425 assert_eq!(decode_query("%zz"), None);
426 assert_eq!(decode_query("%ff"), None);
427 }
428
429 #[test]
430 fn test_out_refuses_bad_fields() {
431 let mut o = Out::default();
432 o.header("x-ok", b"v");
433 assert!(!o.invalid);
434 o.header("content-length", b"1");
435 assert!(o.invalid);
436 let mut o = Out::default();
437 o.header("x", b"a\r\nb");
438 assert!(o.invalid);
439 let mut o = Out::default();
440 o.header("bad name", b"v");
441 assert!(o.invalid);
442 let mut o = Out::default();
443 o.header("Content-Type", b"text/x");
444 assert!(o.has_type && o.has("content-type"));
445 }
446}