1use std::borrow::Cow;
5use std::cell::Cell;
6use std::fmt;
7use std::panic::{AssertUnwindSafe, catch_unwind};
8use std::sync::Arc;
9
10use serve_net::http_net::MethodSet;
11use serve_net::{
12 Envelope, HttpHead, HttpReply, HttpRequest, HttpService, Method, Outcome, Scheme, Verdict,
13 WorkClass,
14};
15
16use crate::cx::{Out, is_local_target};
17use crate::error::{self, BuildError, Problem};
18use crate::route::{Access, BoxHandler, Group, Route};
19use crate::router::{self, Captures, Trie};
20use crate::url::{self, TrailingSlash};
21use crate::{
22 Cx, Denial, DenialKind, Event, EventKind, Events, Fields, Gate, NoEvents, Open, Output, Render,
23 Request, RequestId, Result, Stage, TemplateId, Value,
24};
25
26pub const DEFAULT_PAGE_LIMIT: usize = 8 << 20;
28
29const ERROR_STATUSES: [u16; 14] = [
31 400, 401, 403, 404, 405, 409, 410, 413, 421, 422, 429, 500, 501, 503,
32];
33
34#[non_exhaustive]
37#[derive(Clone, Copy, Debug, PartialEq, Eq)]
38pub enum Hook {
39 Routed,
42 Finish,
45}
46
47#[derive(Clone, Debug)]
49pub struct Routes(Selector);
50
51#[derive(Clone, Debug)]
52enum Selector {
53 All,
54 Prefix(String),
55 Named(Vec<&'static str>),
56}
57
58impl Routes {
59 #[must_use]
61 pub fn all() -> Self {
62 Self(Selector::All)
63 }
64
65 #[must_use]
67 pub fn prefix(prefix: impl Into<String>) -> Self {
68 Self(Selector::Prefix(prefix.into()))
69 }
70
71 #[must_use]
73 pub fn named(names: &[&'static str]) -> Self {
74 Self(Selector::Named(names.to_vec()))
75 }
76
77 fn matches(&self, pattern: &str, name: Option<&str>) -> bool {
78 match &self.0 {
79 Selector::All => true,
80 Selector::Prefix(p) => {
81 let p = p.trim_end_matches('/');
82 pattern == p
83 || pattern
84 .strip_prefix(p)
85 .is_some_and(|rest| rest.starts_with('/'))
86 }
87 Selector::Named(names) => name.is_some_and(|n| names.contains(&n)),
88 }
89 }
90}
91
92type HookFn<I> = Arc<dyn Fn(&mut Cx<'_, I>) -> Result + Send + Sync>;
93
94#[derive(Clone, Copy, Debug, PartialEq, Eq)]
96pub struct Classified {
97 pub route: Option<RouteId>,
99 pub work: WorkClass,
101}
102
103#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
106pub struct RouteId(pub u32);
107
108pub(crate) struct Shared {
110 pub(crate) render: Option<Box<dyn Render>>,
111 pub(crate) checkpoint: Option<Box<dyn crate::Checkpoint>>,
112 pub(crate) events: Box<dyn Events>,
113 pub(crate) page_limit: usize,
114 pub(crate) base: Box<str>,
115 errors: Vec<(u16, TemplateId)>,
116 sign_in: Option<Box<str>>,
117 challenge: Box<str>,
118 wipe: bool,
119}
120
121#[derive(Clone, Debug, Default)]
122struct Site {
123 hosts: Vec<Box<str>>,
125 aliases: Vec<Box<str>>,
126 development: bool,
127 slash: TrailingSlash,
128 lowercase: bool,
129 https_only: bool,
130}
131
132struct Entry<G: Gate> {
133 handler: BoxHandler<G::Identity>,
134 access: Access<G::Rule>,
135 names: Box<[Box<str>]>,
136 pattern: Box<str>,
137 work: WorkClass,
138 template: Option<TemplateId>,
139 routed: Box<[HookFn<G::Identity>]>,
140 finish: Box<[HookFn<G::Identity>]>,
141}
142
143struct Endpoint {
144 methods: [Option<u32>; 10],
146 allow: Box<str>,
147}
148
149pub struct App<G: Gate = Open> {
178 gate: G,
179 shared: Shared,
180 site: Site,
181 trie: Trie,
182 endpoints: Vec<Endpoint>,
183 entries: Vec<Entry<G>>,
184 allow_all: Box<str>,
185}
186
187impl App<Open> {
188 #[must_use]
191 pub fn builder() -> Builder<Open> {
192 Builder::new(Open)
193 }
194}
195
196impl<G: Gate> App<G> {
197 #[must_use]
199 pub fn with_gate(gate: G) -> Builder<G> {
200 Builder::new(gate)
201 }
202}
203
204pub struct Builder<G: Gate> {
207 gate: G,
208 routes: Vec<Route<G::Identity, G::Rule>>,
209 hooks: Vec<(Hook, Routes, HookFn<G::Identity>)>,
210 site: Site,
211 base: String,
212 render: Option<Box<dyn Render>>,
213 checkpoint: Option<Box<dyn crate::Checkpoint>>,
214 events: Box<dyn Events>,
215 page_limit: usize,
216 sign_in: Option<String>,
217 challenge: String,
218 wipe: bool,
219}
220
221impl<G: Gate> Builder<G> {
222 fn new(gate: G) -> Self {
223 Self {
224 gate,
225 routes: Vec::new(),
226 hooks: Vec::new(),
227 site: Site::default(),
228 base: String::new(),
229 render: None,
230 checkpoint: None,
231 events: Box::new(NoEvents),
232 page_limit: DEFAULT_PAGE_LIMIT,
233 sign_in: None,
234 challenge: "Session".into(),
235 wipe: false,
236 }
237 }
238
239 #[must_use]
241 pub fn route(mut self, route: Route<G::Identity, G::Rule>) -> Self {
242 self.routes.push(route);
243 self
244 }
245
246 #[must_use]
248 pub fn group(mut self, group: Group<G::Identity, G::Rule>) -> Self {
249 self.routes.extend(group.into_routes());
250 self
251 }
252
253 #[must_use]
255 pub fn hook<F>(mut self, hook: Hook, routes: Routes, f: F) -> Self
256 where
257 F: Fn(&mut Cx<'_, G::Identity>) -> Result + Send + Sync + 'static,
258 {
259 self.hooks.push((hook, routes, Arc::new(f)));
260 self
261 }
262
263 #[must_use]
267 pub fn host(mut self, host: &str) -> Self {
268 self.site.hosts.push(host.to_ascii_lowercase().into());
269 self
270 }
271
272 #[must_use]
275 pub fn alias(mut self, host: &str) -> Self {
276 self.site.aliases.push(host.to_ascii_lowercase().into());
277 self
278 }
279
280 #[must_use]
285 pub fn development(mut self) -> Self {
286 self.site.development = true;
287 self
288 }
289
290 #[must_use]
294 pub fn base(mut self, base: &str) -> Self {
295 self.base = base.to_owned();
296 self
297 }
298
299 #[must_use]
301 pub fn trailing_slash(mut self, policy: TrailingSlash) -> Self {
302 self.site.slash = policy;
303 self
304 }
305
306 #[must_use]
308 pub fn lowercase_paths(mut self) -> Self {
309 self.site.lowercase = true;
310 self
311 }
312
313 #[must_use]
316 pub fn https_only(mut self) -> Self {
317 self.site.https_only = true;
318 self
319 }
320
321 #[must_use]
325 pub fn sign_in(mut self, path: &str) -> Self {
326 self.sign_in = Some(path.to_owned());
327 self
328 }
329
330 #[must_use]
333 pub fn challenge(mut self, challenge: &str) -> Self {
334 challenge.clone_into(&mut self.challenge);
335 self
336 }
337
338 #[must_use]
340 pub fn render(mut self, engine: impl Render) -> Self {
341 self.render = Some(Box::new(engine));
342 self
343 }
344
345 #[must_use]
347 pub fn events(mut self, events: impl Events) -> Self {
348 self.events = Box::new(events);
349 self
350 }
351
352 #[must_use]
354 pub fn checkpoint(mut self, checkpoint: impl crate::Checkpoint) -> Self {
355 self.checkpoint = Some(Box::new(checkpoint));
356 self
357 }
358
359 #[must_use]
361 pub fn page_limit(mut self, bytes: usize) -> Self {
362 self.page_limit = bytes;
363 self
364 }
365
366 #[must_use]
370 pub fn wipe_buffers(mut self) -> Self {
371 self.wipe = true;
372 self
373 }
374
375 pub fn build(self) -> std::result::Result<App<G>, BuildError> {
383 let mut problems = Vec::new();
384 let mut problem = |subject: &str, message: String| {
385 problems.push(Problem {
386 subject: subject.to_owned(),
387 message,
388 });
389 };
390
391 let base = self.base.trim_end_matches('/');
393 if !base.is_empty() {
394 match url::normalize(base, self.site.lowercase) {
395 Ok(n) if n == base && !base.contains(['?', '#']) => {}
396 _ => problem(
397 "base",
398 format!("'{}' is not a canonical path such as '/blog'", self.base),
399 ),
400 }
401 }
402 for h in self.site.hosts.iter().chain(&self.site.aliases) {
403 if !valid_host(h) {
404 problem("host", format!("'{h}' is not a host name or address"));
405 }
406 }
407 if self.site.https_only && self.site.hosts.is_empty() {
408 problem(
409 "https_only",
410 "redirecting to HTTPS needs a declared host; add .host(\"example.com\")".into(),
411 );
412 }
413 if let Some(s) = &self.sign_in {
414 if !is_local_target(s) {
415 problem("sign_in", format!("'{s}' is not a site-relative path"));
416 }
417 }
418 if self.challenge.is_empty() || !self.challenge.bytes().all(|b| (0x20..0x7f).contains(&b)) {
419 problem("challenge", "the challenge must be visible ASCII".into());
420 }
421
422 let mut errors = Vec::new();
424 if let Some(r) = &self.render {
425 for s in ERROR_STATUSES {
426 if let Some(t) = r.template(&format!("error/{s}")) {
427 errors.push((s, t));
428 }
429 }
430 }
431
432 let mut trie = Trie::default();
434 let mut endpoints: Vec<Endpoint> = Vec::new();
435 let mut entries: Vec<Entry<G>> = Vec::new();
436 let mut all = MethodSet::EMPTY;
437 for r in self.routes {
438 let parts = match router::parse(&r.pattern) {
439 Ok(p) => p,
440 Err(e) => {
441 problem(&r.pattern, e.message());
442 continue;
443 }
444 };
445 if r.handlers.is_empty() {
446 problem(&r.pattern, "the route has no handler".into());
447 continue;
448 }
449 if matches!(r.access, Access::Undeclared) {
450 problem(
451 &r.pattern,
452 "the route declares no access; add .public() or .allow(rule)".into(),
453 );
454 continue;
455 }
456 let template = match (r.template, &self.render) {
457 (None, _) => None,
458 (Some(t), None) => {
459 problem(
460 &r.pattern,
461 format!("template '{t}' is declared but no render engine is set"),
462 );
463 None
464 }
465 (Some(t), Some(engine)) => {
466 let id = engine.template(t);
467 if id.is_none() {
468 problem(&r.pattern, format!("the engine has no template '{t}'"));
469 }
470 id
471 }
472 };
473 let ep = trie.insert(&parts, endpoints.len() as u32) as usize;
474 if ep == endpoints.len() {
475 endpoints.push(Endpoint {
476 methods: [None; 10],
477 allow: Box::from(""),
478 });
479 }
480 let names: Box<[Box<str>]> = router::names(&parts).into();
481 for (m, h) in r.handlers {
482 let subject = format!("{} {}", m.as_str().unwrap_or("?"), r.pattern);
483 if m == Method::Other || m == Method::Connect || m == Method::Trace {
484 problem(&subject, "this method cannot be routed".into());
485 continue;
486 }
487 let slot = &mut endpoints[ep].methods[m.index() as usize];
488 if slot.is_some() {
489 problem(
490 &subject,
491 "a handler for this method and path exists already".into(),
492 );
493 continue;
494 }
495 *slot = Some(entries.len() as u32);
496 let pick = |at: Hook| -> Box<[HookFn<G::Identity>]> {
497 self.hooks
498 .iter()
499 .filter(|(h, sel, _)| *h == at && sel.matches(&r.pattern, r.name))
500 .map(|(_, _, f)| Arc::clone(f))
501 .collect()
502 };
503 entries.push(Entry {
504 handler: h,
505 access: r.access.clone(),
506 names: names.clone(),
507 pattern: r.pattern.as_str().into(),
508 work: r.work,
509 template,
510 routed: pick(Hook::Routed),
511 finish: pick(Hook::Finish),
512 });
513 }
514 }
515 for e in &mut endpoints {
516 let mut set = MethodSet::EMPTY;
517 for m in [
518 Method::Get,
519 Method::Head,
520 Method::Post,
521 Method::Put,
522 Method::Delete,
523 Method::Options,
524 Method::Patch,
525 ] {
526 if e.methods[m.index() as usize].is_some() {
527 set = set.with(m);
528 }
529 }
530 if set.contains(Method::Get) {
531 set = set.with(Method::Head);
532 }
533 set = set.with(Method::Options);
534 all = all.union(set);
535 let mut v = Vec::new();
536 set.write_allow(&mut v);
537 e.allow = String::from_utf8(v).unwrap_or_default().into();
538 }
539 if !problems.is_empty() {
540 return Err(BuildError::new(problems));
541 }
542 let mut v = Vec::new();
543 all.with(Method::Options).write_allow(&mut v);
544 Ok(App {
545 gate: self.gate,
546 shared: Shared {
547 render: self.render,
548 checkpoint: self.checkpoint,
549 events: self.events,
550 page_limit: self.page_limit,
551 base: base.into(),
552 errors,
553 sign_in: self.sign_in.map(Into::into),
554 challenge: self.challenge.into(),
555 wipe: self.wipe,
556 },
557 site: self.site,
558 trie,
559 endpoints,
560 entries,
561 allow_all: String::from_utf8(v).unwrap_or_default().into(),
562 })
563 }
564}
565
566impl<G: Gate> fmt::Debug for Builder<G> {
567 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
568 f.debug_struct("Builder")
569 .field("routes", &self.routes)
570 .field("site", &self.site)
571 .field("base", &self.base)
572 .finish_non_exhaustive()
573 }
574}
575
576fn valid_host(h: &str) -> bool {
577 let (host, port) = split_port(h);
578 let host_ok = if let Some(inner) = host.strip_prefix('[').and_then(|r| r.strip_suffix(']')) {
579 inner.parse::<std::net::Ipv6Addr>().is_ok()
580 } else {
581 !host.is_empty()
582 && host.len() <= 253
583 && host
584 .bytes()
585 .all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'.')
586 };
587 host_ok && port.is_none_or(|p| !p.is_empty() && p.parse::<u16>().is_ok())
588}
589
590fn split_port(a: &str) -> (&str, Option<&str>) {
593 if a.starts_with('[') {
594 return match a.find(']') {
595 Some(i) => match a[i + 1..].strip_prefix(':') {
596 Some(p) => (&a[..=i], Some(p)),
597 None => (a, None),
598 },
599 None => (a, None),
600 };
601 }
602 match a.rsplit_once(':') {
603 Some((h, p)) => (h, Some(p)),
604 None => (a, None),
605 }
606}
607
608fn is_dev_host(host: &str) -> bool {
609 matches!(host, "localhost" | "127.0.0.1" | "[::1]") || host.ends_with(".localhost")
610}
611
612thread_local! {
613 static SCRATCH: Cell<Option<Box<Out>>> = const { Cell::new(None) };
617}
618
619const KEEP_CAPACITY: usize = 1 << 20;
621
622enum Placed<'a> {
624 Here(Cow<'a, str>),
625 Redirect(String),
626 Refuse(u16, &'static str, Option<url::UrlError>),
627}
628
629impl<G: Gate> App<G> {
630 pub fn respond(&self, req: &HttpRequest<'_>, env: &Envelope, reply: &mut HttpReply) -> Outcome {
632 self.run(req, env, None, reply)
633 }
634
635 pub fn respond_identified(
640 &self,
641 req: &HttpRequest<'_>,
642 env: &Envelope,
643 who: &G::Identity,
644 reply: &mut HttpReply,
645 ) -> Outcome {
646 self.run(req, env, Some(who), reply)
647 }
648
649 #[must_use]
652 pub fn classify(&self, head: &HttpHead<'_>) -> Classified {
653 let none = Classified {
654 route: None,
655 work: WorkClass::Quick,
656 };
657 let Placed::Here(path) = self.place(head.authority(), head.path(), Scheme::Https, true)
658 else {
659 return none;
660 };
661 let Some((ep, _)) = self.trie.find(&path) else {
662 return none;
663 };
664 match self.entry_for(ep as usize, head.method()) {
665 Some(i) => Classified {
666 route: Some(RouteId(i)),
667 work: self.entries[i as usize].work,
668 },
669 None => none,
670 }
671 }
672
673 #[must_use]
675 pub fn gate(&self) -> &G {
676 &self.gate
677 }
678
679 fn entry_for(&self, ep: usize, m: Method) -> Option<u32> {
680 let methods = &self.endpoints[ep].methods;
681 match m {
682 Method::Other => None,
683 Method::Head => {
684 methods[Method::Head.index() as usize].or(methods[Method::Get.index() as usize])
685 }
686 m => methods[m.index() as usize],
687 }
688 }
689
690 fn place<'a>(
693 &self,
694 authority: Option<&'a str>,
695 raw: &'a str,
696 scheme: Scheme,
697 quiet: bool,
698 ) -> Placed<'a> {
699 let site = &self.site;
700 let authority = authority.unwrap_or("");
701 let lower = authority.to_ascii_lowercase();
702 let lower = lower.trim_end_matches('.');
703 let host_only = split_port(lower).0.trim_end_matches('.');
704 let listed = |list: &[Box<str>]| {
705 list.iter().any(|h| {
706 let h = h.as_ref();
707 h == lower || (split_port(h).1.is_none() && h == host_only)
708 })
709 };
710 let dev = site.development && is_dev_host(host_only);
711 let mut target_host: Option<&str> = None;
712 if !site.hosts.is_empty() && !dev && !authority.is_empty() && !listed(&site.hosts) {
713 if listed(&site.aliases) {
714 target_host = site.hosts.first().map(AsRef::as_ref);
715 } else {
716 return Placed::Refuse(421, "host.unknown", None);
717 }
718 }
719 let mut https = scheme == Scheme::Https;
720 if site.https_only && !https && !dev {
721 https = true;
722 target_host = target_host.or(site.hosts.first().map(AsRef::as_ref));
723 }
724
725 let canon = match url::normalize(raw, site.lowercase) {
727 Ok(c) => c,
728 Err(e) => return Placed::Refuse(400, e.code(), Some(e)),
729 };
730 let base = self.shared.base.as_ref();
731 let c: &str = &canon;
732 let rel_start = if base.is_empty() {
733 0
734 } else if c == base {
735 c.len()
736 } else if c.strip_prefix(base).is_some_and(|r| r.starts_with('/')) {
737 base.len()
738 } else {
739 return Placed::Refuse(404, "site.outside_base", None);
740 };
741 let rel = &c[rel_start..];
742 let mut fixed: Option<String> = None;
743 if rel.is_empty() {
744 fixed = Some("/".into());
745 } else if rel.len() > 1 {
746 match site.slash {
747 TrailingSlash::Strip => {
748 if let Some(s) = rel.strip_suffix('/') {
749 fixed = Some(s.to_owned());
750 }
751 }
752 TrailingSlash::Add => {
753 let last = rel.rsplit('/').next().unwrap_or("");
754 if !rel.ends_with('/') && !last.contains('.') {
755 fixed = Some(format!("{rel}/"));
756 }
757 }
758 TrailingSlash::Ignore => {}
759 }
760 }
761 if target_host.is_none() && fixed.is_none() {
762 if let Cow::Borrowed(c) = canon {
763 let mut path = &c[rel_start..];
764 if site.slash == TrailingSlash::Ignore && path.len() > 1 {
765 path = path.strip_suffix('/').unwrap_or(path);
766 }
767 return Placed::Here(Cow::Borrowed(path));
768 }
769 }
770 if quiet {
771 return Placed::Refuse(0, "", None);
772 }
773 let rel_out = fixed.as_deref().unwrap_or(rel);
774 let mut loc = String::with_capacity(raw.len() + 32);
775 if let Some(h) = target_host {
776 loc.push_str(if https { "https://" } else { "http://" });
777 loc.push_str(h);
778 }
779 loc.push_str(base);
780 loc.push_str(rel_out);
781 Placed::Redirect(loc)
782 }
783
784 fn run(
785 &self,
786 http: &HttpRequest<'_>,
787 env: &Envelope,
788 who: Option<&G::Identity>,
789 reply: &mut HttpReply,
790 ) -> Outcome {
791 let method = http.method();
792 let raw_path = http.path();
793 if raw_path == "*" {
794 if method == Method::Options {
795 reply.status(204).header("allow", self.allow_all.as_bytes());
796 } else {
797 self.refuse(
798 400,
799 "url.asterisk",
800 EventKind::Refused,
801 None,
802 None,
803 env,
804 reply,
805 );
806 }
807 return Outcome::Done;
808 }
809 let authority = http.head().authority();
810 let host_raw = authority.unwrap_or("");
811 let placed = self.place(authority, raw_path, env.client().scheme(), false);
812 let path = match placed {
813 Placed::Here(path) => path,
814 Placed::Redirect(mut loc) => {
815 if let Some(q) = http.query() {
816 loc.push('?');
817 loc.push_str(q);
818 }
819 let status = if matches!(method, Method::Get | Method::Head) {
820 301
821 } else {
822 308
823 };
824 reply.status(status).header("location", loc.as_bytes());
825 return Outcome::Done;
826 }
827 Placed::Refuse(status, code, src) => {
828 let src = src
829 .as_ref()
830 .map(|e| e as &(dyn std::error::Error + 'static));
831 self.refuse(status, code, EventKind::Refused, None, src, env, reply);
832 return Outcome::Done;
833 }
834 };
835 let mut req = Request {
836 http,
837 env,
838 path: &path,
839 host: host_raw,
840 route: None,
841 };
842
843 let owned;
845 let who: &G::Identity = match who {
846 Some(w) => w,
847 None => match self.gate.authenticate(&req) {
848 Ok(w) => {
849 owned = w;
850 &owned
851 }
852 Err(d) => {
853 self.deny(d, &req, reply);
854 return Outcome::Done;
855 }
856 },
857 };
858
859 let Some((ep, caps)) = self.trie.find(&path) else {
861 self.refuse(
862 404,
863 "route.none",
864 EventKind::Refused,
865 None,
866 None,
867 env,
868 reply,
869 );
870 return Outcome::Done;
871 };
872 let endpoint = &self.endpoints[ep as usize];
873 let Some(idx) = self.entry_for(ep as usize, method) else {
874 if method == Method::Options {
875 reply.status(204).header("allow", endpoint.allow.as_bytes());
876 return Outcome::Done;
877 }
878 let status = if method == Method::Other { 501 } else { 405 };
879 let code = if status == 501 {
880 "method.unknown"
881 } else {
882 "method.not_allowed"
883 };
884 self.refuse(status, code, EventKind::Refused, None, None, env, reply);
885 if status == 405 {
886 reply.header("allow", endpoint.allow.as_bytes());
887 }
888 return Outcome::Done;
889 };
890 let entry = &self.entries[idx as usize];
891 req.route = Some(&entry.pattern);
892
893 if let Access::Rule(rule) = &entry.access {
895 if let Err(d) = self.gate.authorize(who, rule, &req) {
896 self.deny(d, &req, reply);
897 return Outcome::Done;
898 }
899 }
900 if let Some(cp) = &self.shared.checkpoint {
901 match cp.check(env, Stage::Authorized) {
902 Verdict::Pass => {}
903 Verdict::Deny { status } => {
904 self.refuse(
905 status,
906 "checkpoint.denied",
907 EventKind::Revoked,
908 req.route,
909 None,
910 env,
911 reply,
912 );
913 return Outcome::Done;
914 }
915 _ => {
916 self.refuse(
917 403,
918 "checkpoint.killed",
919 EventKind::Revoked,
920 req.route,
921 None,
922 env,
923 reply,
924 );
925 return Outcome::Close;
926 }
927 }
928 }
929
930 let mut out = SCRATCH.with(Cell::take).unwrap_or_default();
932 out.reset();
933 let outcome = self.handle(entry, req, who, caps, &mut out, reply);
934 if self.shared.wipe {
935 out.wipe();
936 }
937 if out.body.capacity() <= KEEP_CAPACITY && out.values.capacity() <= KEEP_CAPACITY {
938 SCRATCH.with(|s| s.set(Some(out)));
939 }
940 outcome
941 }
942
943 fn handle(
944 &self,
945 entry: &Entry<G>,
946 req: Request<'_>,
947 who: &G::Identity,
948 caps: Captures,
949 out: &mut Out,
950 reply: &mut HttpReply,
951 ) -> Outcome {
952 let env = req.env;
953 let route = req.route;
954 let result = {
955 let mut cx = Cx {
956 req,
957 who,
958 caps,
959 names: &entry.names,
960 out: &mut *out,
961 shared: &self.shared,
962 template: entry.template,
963 };
964 catch_unwind(AssertUnwindSafe(|| -> Result {
965 for h in entry.routed.iter() {
966 h(&mut cx)?;
967 }
968 entry.handler.call(&mut cx)?;
969 for h in entry.finish.iter() {
970 h(&mut cx)?;
971 }
972 Ok(())
973 }))
974 };
975 match result {
976 Ok(Ok(())) if !out.invalid => {
977 reply.status(out.status);
978 for &(ns, ne, vs, ve) in &out.spans {
979 let name = core::str::from_utf8(&out.names[ns as usize..ne as usize])
980 .unwrap_or("x-invalid");
981 reply.header(name, &out.values[vs as usize..ve as usize]);
982 }
983 if !out.has("x-content-type-options") {
984 reply.header("x-content-type-options", "nosniff");
985 }
986 reply.body(&out.body);
987 Outcome::Done
988 }
989 Ok(Ok(())) => {
990 out.reset();
991 self.refuse(
992 500,
993 "reply.invalid_field",
994 EventKind::Failed,
995 route,
996 None,
997 env,
998 reply,
999 );
1000 Outcome::Done
1001 }
1002 Ok(Err(e)) => {
1003 out.reset();
1004 let src = std::error::Error::source(&e);
1005 self.refuse(
1006 e.status(),
1007 e.code(),
1008 EventKind::Failed,
1009 route,
1010 src,
1011 env,
1012 reply,
1013 );
1014 Outcome::Done
1015 }
1016 Err(_) => {
1017 out.reset();
1018 self.refuse(
1019 500,
1020 "handler.panic",
1021 EventKind::Panicked,
1022 route,
1023 None,
1024 env,
1025 reply,
1026 );
1027 Outcome::Done
1028 }
1029 }
1030 }
1031
1032 fn deny(&self, d: Denial, req: &Request<'_>, reply: &mut HttpReply) {
1034 let env = req.env;
1035 if d.kind() == DenialKind::SignIn {
1036 if let Some(page) = &self.shared.sign_in {
1037 if matches!(req.method(), Method::Get | Method::Head) {
1038 let mut loc = String::with_capacity(64);
1039 loc.push_str(&self.shared.base);
1040 loc.push_str(page);
1041 loc.push_str(if page.contains('?') {
1042 "&next="
1043 } else {
1044 "?next="
1045 });
1046 let mut next = String::new();
1047 next.push_str(&self.shared.base);
1048 next.push_str(req.path);
1049 if let Some(q) = req.query() {
1050 next.push('?');
1051 next.push_str(q);
1052 }
1053 encode_component(&next, &mut loc);
1054 self.event(EventKind::Denied, env, req.route, 303, d.code(), None);
1055 reply.status(303).header("location", loc.as_bytes());
1056 return;
1057 }
1058 }
1059 }
1060 let status = d.status_code();
1061 self.refuse(
1062 status,
1063 d.code(),
1064 EventKind::Denied,
1065 req.route,
1066 None,
1067 env,
1068 reply,
1069 );
1070 if status == 401 {
1071 reply.header("www-authenticate", self.shared.challenge.as_bytes());
1072 }
1073 }
1074
1075 fn event(
1076 &self,
1077 kind: EventKind,
1078 env: &Envelope,
1079 route: Option<&str>,
1080 status: u16,
1081 code: &'static str,
1082 source: Option<&(dyn std::error::Error + 'static)>,
1083 ) {
1084 self.shared.events.event(&Event {
1085 kind,
1086 id: RequestId::of(env),
1087 route,
1088 status,
1089 code,
1090 source,
1091 });
1092 }
1093
1094 #[allow(clippy::too_many_arguments)]
1098 fn refuse(
1099 &self,
1100 status: u16,
1101 code: &'static str,
1102 kind: EventKind,
1103 route: Option<&str>,
1104 source: Option<&(dyn std::error::Error + 'static)>,
1105 env: &Envelope,
1106 reply: &mut HttpReply,
1107 ) {
1108 self.event(kind, env, route, status, code, source);
1109 let reference = RequestId::of(env).to_string();
1110 let mut body = Vec::with_capacity(512);
1111 let mut done = false;
1112 if let (Some(engine), Some(&(_, t))) = (
1113 self.shared.render.as_deref(),
1114 self.shared.errors.iter().find(|(s, _)| *s == status),
1115 ) {
1116 let fields = [
1117 ("status", Value::Int(i64::from(status))),
1118 ("title", Value::Text(error::title(status))),
1119 ("message", Value::Text(error::message(status))),
1120 ("reference", Value::Text(&reference)),
1121 ];
1122 let view = Fields(&fields);
1123 let mut out = Output::new(&mut body, self.shared.page_limit);
1124 if engine.render(t, &view, &mut out).is_ok() {
1125 done = true;
1126 } else {
1127 body.clear();
1128 }
1129 }
1130 if !done {
1131 error::builtin_page(status, &reference, &mut body);
1132 }
1133 reply
1134 .status(status)
1135 .header("content-type", "text/html; charset=utf-8")
1136 .header("x-content-type-options", "nosniff")
1137 .header("cache-control", "no-store")
1138 .body(&body);
1139 }
1140}
1141
1142impl<G: Gate> HttpService for App<G> {
1143 fn call(&self, request: &HttpRequest<'_>, env: &Envelope, reply: &mut HttpReply) -> Outcome {
1144 self.respond(request, env, reply)
1145 }
1146}
1147
1148impl<G: Gate> fmt::Debug for App<G> {
1149 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1150 f.debug_struct("App")
1151 .field("site", &self.site)
1152 .field("base", &self.shared.base)
1153 .field("routes", &self.entries.len())
1154 .finish_non_exhaustive()
1155 }
1156}
1157
1158fn encode_component(s: &str, out: &mut String) {
1160 const HEX: &[u8; 16] = b"0123456789ABCDEF";
1161 for b in s.bytes() {
1162 if b.is_ascii_alphanumeric() || matches!(b, b'-' | b'.' | b'_' | b'~' | b'/') {
1163 out.push(b as char);
1164 } else {
1165 out.push('%');
1166 out.push(HEX[(b >> 4) as usize] as char);
1167 out.push(HEX[(b & 15) as usize] as char);
1168 }
1169 }
1170}
1171
1172#[cfg(test)]
1173mod tests {
1174 use super::*;
1175
1176 #[test]
1177 fn test_split_port() {
1178 assert_eq!(split_port("a.test:80"), ("a.test", Some("80")));
1179 assert_eq!(split_port("a.test"), ("a.test", None));
1180 assert_eq!(split_port("[::1]:8080"), ("[::1]", Some("8080")));
1181 assert_eq!(split_port("[::1]"), ("[::1]", None));
1182 }
1183
1184 #[test]
1185 fn test_valid_host() {
1186 for ok in ["example.com", "localhost:8080", "[::1]", "127.0.0.1:80"] {
1187 assert!(valid_host(ok), "{ok}");
1188 }
1189 for bad in ["", "a b", "a:", "a:x", "[zz]", "a/b"] {
1190 assert!(!valid_host(bad), "{bad}");
1191 }
1192 }
1193
1194 #[test]
1195 fn test_selector() {
1196 assert!(Routes::prefix("/blog").matches("/blog", None));
1197 assert!(Routes::prefix("/blog/").matches("/blog/:slug", None));
1198 assert!(!Routes::prefix("/blog").matches("/blogger", None));
1199 assert!(Routes::named(&["a"]).matches("/x", Some("a")));
1200 assert!(!Routes::named(&["a"]).matches("/x", None));
1201 }
1202}