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::assets::{self, Assets};
17use crate::csrf;
18use crate::cx::{Out, is_local_target};
19use crate::error::{self, BuildError, Problem};
20use crate::headers::SecurityHeaders;
21use crate::route::{Access, BoxHandler, Group, Route};
22use crate::router::{self, Captures, Trie};
23use crate::url::{self, TrailingSlash};
24use crate::{
25 Cx, Denial, DenialKind, Event, EventKind, Events, Fields, Gate, Identity, NoEvents, Open,
26 Output, Render, Request, RequestId, Result, Stage, TemplateId, Value,
27};
28
29pub const DEFAULT_PAGE_LIMIT: usize = 8 << 20;
31
32const ERROR_STATUSES: [u16; 14] = [
34 400, 401, 403, 404, 405, 409, 410, 413, 421, 422, 429, 500, 501, 503,
35];
36
37#[non_exhaustive]
40#[derive(Clone, Copy, Debug, PartialEq, Eq)]
41pub enum Hook {
42 Routed,
45 Finish,
48}
49
50#[derive(Clone, Debug)]
52pub struct Routes(Selector);
53
54#[derive(Clone, Debug)]
55enum Selector {
56 All,
57 Prefix(String),
58 Named(Vec<&'static str>),
59}
60
61impl Routes {
62 #[must_use]
64 pub fn all() -> Self {
65 Self(Selector::All)
66 }
67
68 #[must_use]
70 pub fn prefix(prefix: impl Into<String>) -> Self {
71 Self(Selector::Prefix(prefix.into()))
72 }
73
74 #[must_use]
76 pub fn named(names: &[&'static str]) -> Self {
77 Self(Selector::Named(names.to_vec()))
78 }
79
80 fn matches(&self, pattern: &str, name: Option<&str>) -> bool {
81 match &self.0 {
82 Selector::All => true,
83 Selector::Prefix(p) => {
84 let p = p.trim_end_matches('/');
85 pattern == p
86 || pattern
87 .strip_prefix(p)
88 .is_some_and(|rest| rest.starts_with('/'))
89 }
90 Selector::Named(names) => name.is_some_and(|n| names.contains(&n)),
91 }
92 }
93}
94
95type HookFn<I> = Arc<dyn Fn(&mut Cx<'_, I>) -> Result + Send + Sync>;
96
97#[derive(Clone, Copy, Debug, PartialEq, Eq)]
99pub struct Classified {
100 pub route: Option<RouteId>,
102 pub work: WorkClass,
104}
105
106#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
109pub struct RouteId(pub u32);
110
111pub(crate) struct Shared {
113 pub(crate) render: Option<Box<dyn Render>>,
114 pub(crate) checkpoint: Option<Box<dyn crate::Checkpoint>>,
115 pub(crate) events: Box<dyn Events>,
116 pub(crate) page_limit: usize,
117 pub(crate) base: Box<str>,
118 errors: Vec<(u16, TemplateId)>,
119 sign_in: Option<Box<str>>,
120 challenge: Box<str>,
121 wipe: bool,
122 security: SecurityHeaders,
123 pub(crate) csrf: Option<csrf_sec::Csrf>,
124 pub(crate) redirect_hosts: Vec<Box<str>>,
125}
126
127#[derive(Clone, Copy)]
130pub(crate) struct At<'a> {
131 pub(crate) env: &'a Envelope,
132 pub(crate) tls: bool,
133}
134
135type ReadyFn = Arc<dyn Fn() -> bool + Send + Sync>;
136
137#[derive(Clone)]
138enum Health {
139 Live,
140 Ready(ReadyFn),
141}
142
143#[derive(Clone, Debug, Default)]
144struct Site {
145 hosts: Vec<Box<str>>,
147 aliases: Vec<Box<str>>,
148 development: bool,
149 slash: TrailingSlash,
150 lowercase: bool,
151 https_only: bool,
152}
153
154struct Entry<G: Gate> {
155 handler: BoxHandler<G::Identity>,
156 access: Access<G::Rule>,
157 names: Box<[Box<str>]>,
158 pattern: Box<str>,
159 work: WorkClass,
160 template: Option<TemplateId>,
161 csrf: bool,
162 limits: form_net::Limits,
163 routed: Box<[HookFn<G::Identity>]>,
164 finish: Box<[HookFn<G::Identity>]>,
165}
166
167struct Endpoint {
168 methods: [Option<u32>; 10],
170 allow: Box<str>,
171}
172
173pub struct App<G: Gate = Open> {
202 gate: G,
203 shared: Shared,
204 site: Site,
205 trie: Trie,
206 endpoints: Vec<Endpoint>,
207 entries: Vec<Entry<G>>,
208 allow_all: Box<str>,
209 assets: Vec<Assets>,
210 health: Vec<(Box<str>, Health)>,
211}
212
213impl App<Open> {
214 #[must_use]
217 pub fn builder() -> Builder<Open> {
218 Builder::new(Open)
219 }
220}
221
222impl<G: Gate> App<G> {
223 #[must_use]
225 pub fn with_gate(gate: G) -> Builder<G> {
226 Builder::new(gate)
227 }
228}
229
230pub struct Builder<G: Gate> {
233 gate: G,
234 routes: Vec<Route<G::Identity, G::Rule>>,
235 hooks: Vec<(Hook, Routes, HookFn<G::Identity>)>,
236 site: Site,
237 base: String,
238 render: Option<Box<dyn Render>>,
239 checkpoint: Option<Box<dyn crate::Checkpoint>>,
240 events: Box<dyn Events>,
241 page_limit: usize,
242 sign_in: Option<String>,
243 challenge: String,
244 wipe: bool,
245 security: SecurityHeaders,
246 assets: Vec<Assets>,
247 health: Vec<(String, Health)>,
248 csrf: Option<csrf_sec::Csrf>,
249 redirect_hosts: Vec<Box<str>>,
250}
251
252impl<G: Gate> Builder<G> {
253 fn new(gate: G) -> Self {
254 Self {
255 gate,
256 routes: Vec::new(),
257 hooks: Vec::new(),
258 site: Site::default(),
259 base: String::new(),
260 render: None,
261 checkpoint: None,
262 events: Box::new(NoEvents),
263 page_limit: DEFAULT_PAGE_LIMIT,
264 sign_in: None,
265 challenge: "Session".into(),
266 wipe: false,
267 security: SecurityHeaders::default(),
268 assets: Vec::new(),
269 health: Vec::new(),
270 csrf: None,
271 redirect_hosts: Vec::new(),
272 }
273 }
274
275 #[must_use]
280 pub fn csrf(mut self, csrf: csrf_sec::Csrf) -> Self {
281 self.csrf = Some(csrf);
282 self
283 }
284
285 #[must_use]
288 pub fn redirect_hosts(mut self, hosts: &[&str]) -> Self {
289 self.redirect_hosts.extend(
290 hosts
291 .iter()
292 .map(|h| h.to_ascii_lowercase().into_boxed_str()),
293 );
294 self
295 }
296
297 #[must_use]
300 pub fn assets(mut self, assets: Assets) -> Self {
301 self.assets.push(assets);
302 self
303 }
304
305 #[must_use]
308 pub fn health(mut self, path: &str) -> Self {
309 self.health.push((path.to_owned(), Health::Live));
310 self
311 }
312
313 #[must_use]
317 pub fn ready(mut self, path: &str, check: impl Fn() -> bool + Send + Sync + 'static) -> Self {
318 self.health
319 .push((path.to_owned(), Health::Ready(Arc::new(check))));
320 self
321 }
322
323 #[must_use]
326 pub fn security_headers(mut self, policy: SecurityHeaders) -> Self {
327 self.security = policy;
328 self
329 }
330
331 #[must_use]
333 pub fn route(mut self, route: Route<G::Identity, G::Rule>) -> Self {
334 self.routes.push(route);
335 self
336 }
337
338 #[must_use]
340 pub fn group(mut self, group: Group<G::Identity, G::Rule>) -> Self {
341 self.routes.extend(group.into_routes());
342 self
343 }
344
345 #[must_use]
347 pub fn hook<F>(mut self, hook: Hook, routes: Routes, f: F) -> Self
348 where
349 F: Fn(&mut Cx<'_, G::Identity>) -> Result + Send + Sync + 'static,
350 {
351 self.hooks.push((hook, routes, Arc::new(f)));
352 self
353 }
354
355 #[must_use]
359 pub fn host(mut self, host: &str) -> Self {
360 self.site.hosts.push(host.to_ascii_lowercase().into());
361 self
362 }
363
364 #[must_use]
367 pub fn alias(mut self, host: &str) -> Self {
368 self.site.aliases.push(host.to_ascii_lowercase().into());
369 self
370 }
371
372 #[must_use]
377 pub fn development(mut self) -> Self {
378 self.site.development = true;
379 self
380 }
381
382 #[must_use]
386 pub fn base(mut self, base: &str) -> Self {
387 self.base = base.to_owned();
388 self
389 }
390
391 #[must_use]
393 pub fn trailing_slash(mut self, policy: TrailingSlash) -> Self {
394 self.site.slash = policy;
395 self
396 }
397
398 #[must_use]
400 pub fn lowercase_paths(mut self) -> Self {
401 self.site.lowercase = true;
402 self
403 }
404
405 #[must_use]
408 pub fn https_only(mut self) -> Self {
409 self.site.https_only = true;
410 self
411 }
412
413 #[must_use]
417 pub fn sign_in(mut self, path: &str) -> Self {
418 self.sign_in = Some(path.to_owned());
419 self
420 }
421
422 #[must_use]
425 pub fn challenge(mut self, challenge: &str) -> Self {
426 challenge.clone_into(&mut self.challenge);
427 self
428 }
429
430 #[must_use]
432 pub fn render(mut self, engine: impl Render) -> Self {
433 self.render = Some(Box::new(engine));
434 self
435 }
436
437 #[must_use]
439 pub fn events(mut self, events: impl Events) -> Self {
440 self.events = Box::new(events);
441 self
442 }
443
444 #[must_use]
446 pub fn checkpoint(mut self, checkpoint: impl crate::Checkpoint) -> Self {
447 self.checkpoint = Some(Box::new(checkpoint));
448 self
449 }
450
451 #[must_use]
453 pub fn page_limit(mut self, bytes: usize) -> Self {
454 self.page_limit = bytes;
455 self
456 }
457
458 #[must_use]
462 pub fn wipe_buffers(mut self) -> Self {
463 self.wipe = true;
464 self
465 }
466
467 pub fn build(self) -> std::result::Result<App<G>, BuildError> {
475 let mut problems = Vec::new();
476 let mut problem = |subject: &str, message: String| {
477 problems.push(Problem {
478 subject: subject.to_owned(),
479 message,
480 });
481 };
482
483 let base = self.base.trim_end_matches('/');
485 if !base.is_empty() {
486 match url::normalize(base, self.site.lowercase) {
487 Ok(n) if n == base && !base.contains(['?', '#']) => {}
488 _ => problem(
489 "base",
490 format!("'{}' is not a canonical path such as '/blog'", self.base),
491 ),
492 }
493 }
494 for h in self.site.hosts.iter().chain(&self.site.aliases) {
495 if !valid_host(h) {
496 problem("host", format!("'{h}' is not a host name or address"));
497 }
498 }
499 if self.site.https_only && self.site.hosts.is_empty() {
500 problem(
501 "https_only",
502 "redirecting to HTTPS needs a declared host; add .host(\"example.com\")".into(),
503 );
504 }
505 if let Some(s) = &self.sign_in {
506 if !is_local_target(s) {
507 problem("sign_in", format!("'{s}' is not a site-relative path"));
508 }
509 }
510 if self.challenge.is_empty() || !self.challenge.bytes().all(|b| (0x20..0x7f).contains(&b)) {
511 problem("challenge", "the challenge must be visible ASCII".into());
512 }
513
514 if !self.security.valid() {
515 problem(
516 "security_headers",
517 "every header value must be visible ASCII and not empty".into(),
518 );
519 }
520 let mut assets = self.assets;
521 for a in &mut assets {
522 let subject = format!("assets {}", a.mount);
523 if a.mount.is_empty()
524 || !is_local_target(&a.mount)
525 || url::normalize(&a.mount, self.site.lowercase).map_or(true, |n| n != a.mount)
526 {
527 problem(
528 &subject,
529 "the mount must be a canonical path such as '/static'".into(),
530 );
531 }
532 match std::fs::canonicalize(&a.dir) {
533 Ok(root) if root.is_dir() => a.root = root,
534 _ => problem(
535 &subject,
536 format!("'{}' is not a folder that exists", a.dir.display()),
537 ),
538 }
539 for r in &self.routes {
540 if a.strip(&r.pattern).is_some() {
541 problem(
542 &r.pattern,
543 format!(
544 "the route is under the assets mount '{}' and can never run",
545 a.mount
546 ),
547 );
548 }
549 }
550 }
551 for h in &self.redirect_hosts {
552 if !valid_host(h) {
553 problem(
554 "redirect_hosts",
555 format!("'{h}' is not a host name or address"),
556 );
557 }
558 }
559 let mut health = Vec::new();
560 for (p, h) in self.health {
561 if !is_local_target(&p) || url::normalize(&p, false).map_or(true, |n| n != p) {
562 problem(
563 "health",
564 format!("'{p}' is not a canonical site-relative path"),
565 );
566 }
567 health.push((p.into_boxed_str(), h));
568 }
569
570 let mut errors = Vec::new();
572 if let Some(r) = &self.render {
573 for s in ERROR_STATUSES {
574 if let Some(t) = r.template(&format!("error/{s}")) {
575 errors.push((s, t));
576 }
577 }
578 }
579
580 let mut trie = Trie::default();
582 let mut endpoints: Vec<Endpoint> = Vec::new();
583 let mut entries: Vec<Entry<G>> = Vec::new();
584 let mut all = MethodSet::EMPTY;
585 for r in self.routes {
586 let parts = match router::parse(&r.pattern) {
587 Ok(p) => p,
588 Err(e) => {
589 problem(&r.pattern, e.message());
590 continue;
591 }
592 };
593 if r.handlers.is_empty() {
594 problem(&r.pattern, "the route has no handler".into());
595 continue;
596 }
597 if matches!(r.access, Access::Undeclared) {
598 problem(
599 &r.pattern,
600 "the route declares no access; add .public() or .allow(rule)".into(),
601 );
602 continue;
603 }
604 let template = match (r.template, &self.render) {
605 (None, _) => None,
606 (Some(t), None) => {
607 problem(
608 &r.pattern,
609 format!("template '{t}' is declared but no render engine is set"),
610 );
611 None
612 }
613 (Some(t), Some(engine)) => {
614 let id = engine.template(t);
615 if id.is_none() {
616 problem(&r.pattern, format!("the engine has no template '{t}'"));
617 }
618 id
619 }
620 };
621 let ep = trie.insert(&parts, endpoints.len() as u32) as usize;
622 if ep == endpoints.len() {
623 endpoints.push(Endpoint {
624 methods: [None; 10],
625 allow: Box::from(""),
626 });
627 }
628 let names: Box<[Box<str>]> = router::names(&parts).into();
629 if r.csrf_exempt.is_some_and(|reason| reason.trim().is_empty()) {
630 problem(&r.pattern, "a CSRF exemption needs its reason".into());
631 }
632 if let Err(e) = r.form_limits.validate() {
633 problem(&r.pattern, format!("the form limits are inconsistent: {e}"));
634 }
635 for (m, h) in r.handlers {
636 let subject = format!("{} {}", m.as_str().unwrap_or("?"), r.pattern);
637 if m == Method::Other || m == Method::Connect || m == Method::Trace {
638 problem(&subject, "this method cannot be routed".into());
639 continue;
640 }
641 let slot = &mut endpoints[ep].methods[m.index() as usize];
642 if slot.is_some() {
643 problem(
644 &subject,
645 "a handler for this method and path exists already".into(),
646 );
647 continue;
648 }
649 *slot = Some(entries.len() as u32);
650 let csrf = r.csrf_exempt.is_none() && !m.is_safe();
651 if csrf && self.csrf.is_none() {
652 problem(
653 &subject,
654 "an unsafe method needs CSRF protection; set Builder::csrf(..) or mark the route csrf_exempt(\"reason\")".into(),
655 );
656 }
657 let pick = |at: Hook| -> Box<[HookFn<G::Identity>]> {
658 self.hooks
659 .iter()
660 .filter(|(h, sel, _)| *h == at && sel.matches(&r.pattern, r.name))
661 .map(|(_, _, f)| Arc::clone(f))
662 .collect()
663 };
664 entries.push(Entry {
665 handler: h,
666 access: r.access.clone(),
667 names: names.clone(),
668 pattern: r.pattern.as_str().into(),
669 work: r.work,
670 template,
671 csrf,
672 limits: r.form_limits,
673 routed: pick(Hook::Routed),
674 finish: pick(Hook::Finish),
675 });
676 }
677 }
678 for e in &mut endpoints {
679 let mut set = MethodSet::EMPTY;
680 for m in [
681 Method::Get,
682 Method::Head,
683 Method::Post,
684 Method::Put,
685 Method::Delete,
686 Method::Options,
687 Method::Patch,
688 ] {
689 if e.methods[m.index() as usize].is_some() {
690 set = set.with(m);
691 }
692 }
693 if set.contains(Method::Get) {
694 set = set.with(Method::Head);
695 }
696 set = set.with(Method::Options);
697 all = all.union(set);
698 let mut v = Vec::new();
699 set.write_allow(&mut v);
700 e.allow = String::from_utf8(v).unwrap_or_default().into();
701 }
702 if !problems.is_empty() {
703 return Err(BuildError::new(problems));
704 }
705 let mut v = Vec::new();
706 all.with(Method::Options).write_allow(&mut v);
707 Ok(App {
708 gate: self.gate,
709 shared: Shared {
710 render: self.render,
711 checkpoint: self.checkpoint,
712 events: self.events,
713 page_limit: self.page_limit,
714 base: base.into(),
715 errors,
716 sign_in: self.sign_in.map(Into::into),
717 challenge: self.challenge.into(),
718 wipe: self.wipe,
719 security: self.security,
720 csrf: self.csrf,
721 redirect_hosts: self.redirect_hosts,
722 },
723 site: self.site,
724 trie,
725 endpoints,
726 entries,
727 allow_all: String::from_utf8(v).unwrap_or_default().into(),
728 assets,
729 health,
730 })
731 }
732}
733
734impl<G: Gate> fmt::Debug for Builder<G> {
735 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
736 f.debug_struct("Builder")
737 .field("routes", &self.routes)
738 .field("site", &self.site)
739 .field("base", &self.base)
740 .finish_non_exhaustive()
741 }
742}
743
744fn valid_host(h: &str) -> bool {
745 let (host, port) = split_port(h);
746 let host_ok = if let Some(inner) = host.strip_prefix('[').and_then(|r| r.strip_suffix(']')) {
747 inner.parse::<std::net::Ipv6Addr>().is_ok()
748 } else {
749 !host.is_empty()
750 && host.len() <= 253
751 && host
752 .bytes()
753 .all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'.')
754 };
755 host_ok && port.is_none_or(|p| !p.is_empty() && p.parse::<u16>().is_ok())
756}
757
758fn split_port(a: &str) -> (&str, Option<&str>) {
761 if a.starts_with('[') {
762 return match a.find(']') {
763 Some(i) => match a[i + 1..].strip_prefix(':') {
764 Some(p) => (&a[..=i], Some(p)),
765 None => (a, None),
766 },
767 None => (a, None),
768 };
769 }
770 match a.rsplit_once(':') {
771 Some((h, p)) => (h, Some(p)),
772 None => (a, None),
773 }
774}
775
776fn is_dev_host(host: &str) -> bool {
777 matches!(host, "localhost" | "127.0.0.1" | "[::1]") || host.ends_with(".localhost")
778}
779
780thread_local! {
781 static SCRATCH: Cell<Option<Box<Out>>> = const { Cell::new(None) };
785}
786
787const KEEP_CAPACITY: usize = 1 << 20;
789
790enum Placed<'a> {
792 Here(Cow<'a, str>),
793 Redirect(String),
794 Refuse(u16, &'static str, Option<url::UrlError>),
795}
796
797impl<G: Gate> App<G> {
798 pub fn respond(&self, req: &HttpRequest<'_>, env: &Envelope, reply: &mut HttpReply) -> Outcome {
800 self.run(req, env, None, reply)
801 }
802
803 pub fn respond_identified(
808 &self,
809 req: &HttpRequest<'_>,
810 env: &Envelope,
811 who: &G::Identity,
812 reply: &mut HttpReply,
813 ) -> Outcome {
814 self.run(req, env, Some(who), reply)
815 }
816
817 #[must_use]
820 pub fn classify(&self, head: &HttpHead<'_>) -> Classified {
821 let none = Classified {
822 route: None,
823 work: WorkClass::Quick,
824 };
825 let Placed::Here(path) = self.place(head.authority(), head.path(), Scheme::Https, true)
826 else {
827 return none;
828 };
829 let Some((ep, _)) = self.trie.find(&path) else {
830 return none;
831 };
832 match self.entry_for(ep as usize, head.method()) {
833 Some(i) => Classified {
834 route: Some(RouteId(i)),
835 work: self.entries[i as usize].work,
836 },
837 None => none,
838 }
839 }
840
841 #[must_use]
843 pub fn gate(&self) -> &G {
844 &self.gate
845 }
846
847 fn entry_for(&self, ep: usize, m: Method) -> Option<u32> {
848 let methods = &self.endpoints[ep].methods;
849 match m {
850 Method::Other => None,
851 Method::Head => {
852 methods[Method::Head.index() as usize].or(methods[Method::Get.index() as usize])
853 }
854 m => methods[m.index() as usize],
855 }
856 }
857
858 fn place<'a>(
861 &self,
862 authority: Option<&'a str>,
863 raw: &'a str,
864 scheme: Scheme,
865 quiet: bool,
866 ) -> Placed<'a> {
867 let site = &self.site;
868 let authority = authority.unwrap_or("");
869 let lower = authority.to_ascii_lowercase();
870 let lower = lower.trim_end_matches('.');
871 let host_only = split_port(lower).0.trim_end_matches('.');
872 let listed = |list: &[Box<str>]| {
873 list.iter().any(|h| {
874 let h = h.as_ref();
875 h == lower || (split_port(h).1.is_none() && h == host_only)
876 })
877 };
878 let dev = site.development && is_dev_host(host_only);
879 let mut target_host: Option<&str> = None;
880 if !site.hosts.is_empty() && !dev && !authority.is_empty() && !listed(&site.hosts) {
881 if listed(&site.aliases) {
882 target_host = site.hosts.first().map(AsRef::as_ref);
883 } else {
884 return Placed::Refuse(421, "host.unknown", None);
885 }
886 }
887 let mut https = scheme == Scheme::Https;
888 if site.https_only && !https && !dev {
889 https = true;
890 target_host = target_host.or(site.hosts.first().map(AsRef::as_ref));
891 }
892
893 let canon = match url::normalize(raw, site.lowercase) {
895 Ok(c) => c,
896 Err(e) => return Placed::Refuse(400, e.code(), Some(e)),
897 };
898 let base = self.shared.base.as_ref();
899 let c: &str = &canon;
900 let rel_start = if base.is_empty() {
901 0
902 } else if c == base {
903 c.len()
904 } else if c.strip_prefix(base).is_some_and(|r| r.starts_with('/')) {
905 base.len()
906 } else {
907 return Placed::Refuse(404, "site.outside_base", None);
908 };
909 let rel = &c[rel_start..];
910 let mut fixed: Option<String> = None;
911 if rel.is_empty() {
912 fixed = Some("/".into());
913 } else if rel.len() > 1 {
914 match site.slash {
915 TrailingSlash::Strip => {
916 if let Some(s) = rel.strip_suffix('/') {
917 fixed = Some(s.to_owned());
918 }
919 }
920 TrailingSlash::Add => {
921 let last = rel.rsplit('/').next().unwrap_or("");
922 if !rel.ends_with('/') && !last.contains('.') {
923 fixed = Some(format!("{rel}/"));
924 }
925 }
926 TrailingSlash::Ignore => {}
927 }
928 }
929 if target_host.is_none() && fixed.is_none() {
930 if let Cow::Borrowed(c) = canon {
931 let mut path = &c[rel_start..];
932 if site.slash == TrailingSlash::Ignore && path.len() > 1 {
933 path = path.strip_suffix('/').unwrap_or(path);
934 }
935 return Placed::Here(Cow::Borrowed(path));
936 }
937 }
938 if quiet {
939 return Placed::Refuse(0, "", None);
940 }
941 let rel_out = fixed.as_deref().unwrap_or(rel);
942 let mut loc = String::with_capacity(raw.len() + 32);
943 if let Some(h) = target_host {
944 loc.push_str(if https { "https://" } else { "http://" });
945 loc.push_str(h);
946 }
947 loc.push_str(base);
948 loc.push_str(rel_out);
949 Placed::Redirect(loc)
950 }
951
952 fn run(
953 &self,
954 http: &HttpRequest<'_>,
955 env: &Envelope,
956 who: Option<&G::Identity>,
957 reply: &mut HttpReply,
958 ) -> Outcome {
959 let method = http.method();
960 let raw_path = http.path();
961 let authority = http.head().authority();
962 let at = At {
963 env,
964 tls: self.tls(env, authority),
965 };
966 if raw_path == "*" {
967 if method == Method::Options {
968 reply.status(204).header("allow", self.allow_all.as_bytes());
969 self.secure(reply, at.tls, |_| false);
970 } else {
971 self.refuse(
972 400,
973 "url.asterisk",
974 EventKind::Refused,
975 None,
976 None,
977 at,
978 reply,
979 );
980 }
981 return Outcome::Done;
982 }
983 let host_raw = authority.unwrap_or("");
984 let placed = self.place(authority, raw_path, env.client().scheme(), false);
985 let path = match placed {
986 Placed::Here(path) => path,
987 Placed::Redirect(mut loc) => {
988 if let Some(q) = http.query() {
989 loc.push('?');
990 loc.push_str(q);
991 }
992 let status = if matches!(method, Method::Get | Method::Head) {
993 301
994 } else {
995 308
996 };
997 reply.status(status).header("location", loc.as_bytes());
998 self.secure(reply, at.tls, |_| false);
999 return Outcome::Done;
1000 }
1001 Placed::Refuse(status, code, src) => {
1002 let src = src
1003 .as_ref()
1004 .map(|e| e as &(dyn std::error::Error + 'static));
1005 self.refuse(status, code, EventKind::Refused, None, src, at, reply);
1006 return Outcome::Done;
1007 }
1008 };
1009 if let Some(outcome) = self.early(http, at, &path, reply) {
1011 return outcome;
1012 }
1013
1014 let mut req = Request {
1015 http,
1016 env,
1017 path: &path,
1018 host: host_raw,
1019 route: None,
1020 tls: at.tls,
1021 };
1022
1023 let owned;
1025 let who: &G::Identity = match who {
1026 Some(w) => w,
1027 None => match self.gate.authenticate(&req) {
1028 Ok(w) => {
1029 owned = w;
1030 &owned
1031 }
1032 Err(d) => {
1033 self.deny(d, &req, reply);
1034 return Outcome::Done;
1035 }
1036 },
1037 };
1038
1039 let Some((ep, caps)) = self.trie.find(&path) else {
1041 self.refuse(404, "route.none", EventKind::Refused, None, None, at, reply);
1042 return Outcome::Done;
1043 };
1044 let endpoint = &self.endpoints[ep as usize];
1045 let Some(idx) = self.entry_for(ep as usize, method) else {
1046 if method == Method::Options {
1047 reply.status(204).header("allow", endpoint.allow.as_bytes());
1048 self.secure(reply, at.tls, |_| false);
1049 return Outcome::Done;
1050 }
1051 let status = if method == Method::Other { 501 } else { 405 };
1052 let code = if status == 501 {
1053 "method.unknown"
1054 } else {
1055 "method.not_allowed"
1056 };
1057 self.refuse(status, code, EventKind::Refused, None, None, at, reply);
1058 if status == 405 {
1059 reply.header("allow", endpoint.allow.as_bytes());
1060 }
1061 return Outcome::Done;
1062 };
1063 let entry = &self.entries[idx as usize];
1064 req.route = Some(&entry.pattern);
1065
1066 if let Access::Rule(rule) = &entry.access {
1068 if let Err(d) = self.gate.authorize(who, rule, &req) {
1069 self.deny(d, &req, reply);
1070 return Outcome::Done;
1071 }
1072 }
1073 if let Some(cp) = &self.shared.checkpoint {
1074 match cp.check(env, Stage::Authorized) {
1075 Verdict::Pass => {}
1076 Verdict::Deny { status } => {
1077 self.refuse(
1078 status,
1079 "checkpoint.denied",
1080 EventKind::Revoked,
1081 req.route,
1082 None,
1083 at,
1084 reply,
1085 );
1086 return Outcome::Done;
1087 }
1088 _ => {
1089 self.refuse(
1090 403,
1091 "checkpoint.killed",
1092 EventKind::Revoked,
1093 req.route,
1094 None,
1095 at,
1096 reply,
1097 );
1098 return Outcome::Close;
1099 }
1100 }
1101 }
1102
1103 let binding = who.csrf_binding();
1105 let csrf_state = match (&self.shared.csrf, entry.csrf) {
1106 (Some(engine), true) => {
1107 let scheme = if env.client().scheme() == Scheme::Https {
1108 "https://"
1109 } else {
1110 "http://"
1111 };
1112 let own = format!("{scheme}{}", host_raw.to_ascii_lowercase());
1113 match csrf::check(engine, http, binding, &own, &entry.limits) {
1114 Ok(s) => s,
1115 Err(code) => {
1116 self.refuse(403, code, EventKind::Denied, req.route, None, at, reply);
1117 return Outcome::Done;
1118 }
1119 }
1120 }
1121 (None, true) => {
1123 self.refuse(
1124 500,
1125 "csrf.no_engine",
1126 EventKind::Failed,
1127 req.route,
1128 None,
1129 at,
1130 reply,
1131 );
1132 return Outcome::Done;
1133 }
1134 (_, false) => csrf::State::Off,
1135 };
1136
1137 let mut out = SCRATCH.with(Cell::take).unwrap_or_default();
1139 out.reset();
1140 let outcome = self.handle(
1141 entry,
1142 req,
1143 who,
1144 caps,
1145 (csrf_state, binding),
1146 &mut out,
1147 reply,
1148 );
1149 if self.shared.wipe {
1150 out.wipe();
1151 }
1152 if out.body.capacity() <= KEEP_CAPACITY && out.values.capacity() <= KEEP_CAPACITY {
1153 SCRATCH.with(|s| s.set(Some(out)));
1154 }
1155 outcome
1156 }
1157
1158 fn early(
1160 &self,
1161 http: &HttpRequest<'_>,
1162 at: At<'_>,
1163 path: &str,
1164 reply: &mut HttpReply,
1165 ) -> Option<Outcome> {
1166 let method = http.method();
1167 let get_like = matches!(method, Method::Get | Method::Head);
1168 if let Some((_, h)) = self.health.iter().find(|(p, _)| p.as_ref() == path) {
1169 if !get_like {
1170 self.not_get(method, at, reply);
1171 return Some(Outcome::Done);
1172 }
1173 let (status, text) = match h {
1174 Health::Live => (200, "ok"),
1175 Health::Ready(f) if f() => (200, "ready"),
1176 Health::Ready(_) => (503, "not ready"),
1177 };
1178 reply.text(status, text).header("cache-control", "no-store");
1179 self.secure(reply, at.tls, |_| false);
1180 return Some(Outcome::Done);
1181 }
1182 let (assets, rel) = self
1183 .assets
1184 .iter()
1185 .find_map(|a| a.strip(path).map(|rel| (a, rel)))?;
1186 if !get_like {
1187 self.not_get(method, at, reply);
1188 return Some(Outcome::Done);
1189 }
1190 let Some(found) = assets::find(assets, rel, http.header("accept-encoding")) else {
1191 self.refuse(
1192 404,
1193 "assets.none",
1194 EventKind::Refused,
1195 None,
1196 None,
1197 at,
1198 reply,
1199 );
1200 return Some(Outcome::Done);
1201 };
1202 let outcome = assets::serve(assets, &found, http, reply);
1203 self.secure(reply, at.tls, |_| false);
1204 Some(outcome)
1205 }
1206
1207 fn not_get(&self, method: Method, at: At<'_>, reply: &mut HttpReply) {
1209 if method == Method::Options {
1210 reply.status(204).header("allow", "GET, HEAD, OPTIONS");
1211 self.secure(reply, at.tls, |_| false);
1212 } else {
1213 let (status, code) = if method == Method::Other {
1214 (501, "method.unknown")
1215 } else {
1216 (405, "method.not_allowed")
1217 };
1218 self.refuse(status, code, EventKind::Refused, None, None, at, reply);
1219 if status == 405 {
1220 reply.header("allow", "GET, HEAD, OPTIONS");
1221 }
1222 }
1223 }
1224
1225 #[allow(clippy::too_many_arguments)]
1226 fn handle(
1227 &self,
1228 entry: &Entry<G>,
1229 req: Request<'_>,
1230 who: &G::Identity,
1231 caps: Captures,
1232 (csrf_state, binding): (csrf::State, Option<&[u8]>),
1233 out: &mut Out,
1234 reply: &mut HttpReply,
1235 ) -> Outcome {
1236 let at = At {
1237 env: req.env,
1238 tls: req.tls,
1239 };
1240 let route = req.route;
1241 let (result, unverified) = {
1242 let mut cx = Cx {
1243 req,
1244 who,
1245 caps,
1246 names: &entry.names,
1247 out: &mut *out,
1248 shared: &self.shared,
1249 template: entry.template,
1250 csrf: csrf_state,
1251 binding,
1252 limits: &entry.limits,
1253 };
1254 let result = catch_unwind(AssertUnwindSafe(|| -> Result {
1255 for h in entry.routed.iter() {
1256 h(&mut cx)?;
1257 }
1258 entry.handler.call(&mut cx)?;
1259 for h in entry.finish.iter() {
1260 h(&mut cx)?;
1261 }
1262 Ok(())
1263 }));
1264 (result, cx.csrf == csrf::State::Pending)
1265 };
1266 match result {
1267 Ok(Ok(())) if unverified => {
1268 out.reset();
1271 self.refuse(
1272 403,
1273 "csrf.unverified",
1274 EventKind::Failed,
1275 route,
1276 None,
1277 at,
1278 reply,
1279 );
1280 Outcome::Done
1281 }
1282 Ok(Ok(())) if !out.invalid => {
1283 reply.status(out.status);
1284 for &(ns, ne, vs, ve) in &out.spans {
1285 let name = core::str::from_utf8(&out.names[ns as usize..ne as usize])
1286 .unwrap_or("x-invalid");
1287 reply.header(name, &out.values[vs as usize..ve as usize]);
1288 }
1289 if !out.has("x-content-type-options") {
1290 reply.header("x-content-type-options", "nosniff");
1291 }
1292 self.secure(reply, at.tls, |n| out.has(n));
1293 reply.body(&out.body);
1294 Outcome::Done
1295 }
1296 Ok(Ok(())) => {
1297 out.reset();
1298 self.refuse(
1299 500,
1300 "reply.invalid_field",
1301 EventKind::Failed,
1302 route,
1303 None,
1304 at,
1305 reply,
1306 );
1307 Outcome::Done
1308 }
1309 Ok(Err(e)) => {
1310 out.reset();
1311 let src = std::error::Error::source(&e);
1312 self.refuse(
1313 e.status(),
1314 e.code(),
1315 EventKind::Failed,
1316 route,
1317 src,
1318 at,
1319 reply,
1320 );
1321 Outcome::Done
1322 }
1323 Err(_) => {
1324 out.reset();
1325 self.refuse(
1326 500,
1327 "handler.panic",
1328 EventKind::Panicked,
1329 route,
1330 None,
1331 at,
1332 reply,
1333 );
1334 Outcome::Done
1335 }
1336 }
1337 }
1338
1339 fn deny(&self, d: Denial, req: &Request<'_>, reply: &mut HttpReply) {
1341 let env = req.env;
1342 let at = At { env, tls: req.tls };
1343 if d.kind() == DenialKind::SignIn {
1344 if let Some(page) = &self.shared.sign_in {
1345 if matches!(req.method(), Method::Get | Method::Head) {
1346 let mut loc = String::with_capacity(64);
1347 loc.push_str(&self.shared.base);
1348 loc.push_str(page);
1349 loc.push_str(if page.contains('?') {
1350 "&next="
1351 } else {
1352 "?next="
1353 });
1354 let mut next = String::new();
1355 next.push_str(&self.shared.base);
1356 next.push_str(req.path);
1357 if let Some(q) = req.query() {
1358 next.push('?');
1359 next.push_str(q);
1360 }
1361 encode_component(&next, &mut loc);
1362 self.event(EventKind::Denied, env, req.route, 303, d.code(), None);
1363 reply.status(303).header("location", loc.as_bytes());
1364 self.secure(reply, at.tls, |_| false);
1365 return;
1366 }
1367 }
1368 }
1369 let status = d.status_code();
1370 self.refuse(
1371 status,
1372 d.code(),
1373 EventKind::Denied,
1374 req.route,
1375 None,
1376 at,
1377 reply,
1378 );
1379 if status == 401 {
1380 reply.header("www-authenticate", self.shared.challenge.as_bytes());
1381 }
1382 }
1383
1384 fn event(
1385 &self,
1386 kind: EventKind,
1387 env: &Envelope,
1388 route: Option<&str>,
1389 status: u16,
1390 code: &'static str,
1391 source: Option<&(dyn std::error::Error + 'static)>,
1392 ) {
1393 self.shared.events.event(&Event {
1394 kind,
1395 id: RequestId::of(env),
1396 route,
1397 status,
1398 code,
1399 source,
1400 });
1401 }
1402
1403 #[allow(clippy::too_many_arguments)]
1407 fn refuse(
1408 &self,
1409 status: u16,
1410 code: &'static str,
1411 kind: EventKind,
1412 route: Option<&str>,
1413 source: Option<&(dyn std::error::Error + 'static)>,
1414 at: At<'_>,
1415 reply: &mut HttpReply,
1416 ) {
1417 self.event(kind, at.env, route, status, code, source);
1418 let reference = RequestId::of(at.env).to_string();
1419 let mut body = Vec::with_capacity(512);
1420 let mut done = false;
1421 if let (Some(engine), Some(&(_, t))) = (
1422 self.shared.render.as_deref(),
1423 self.shared.errors.iter().find(|(s, _)| *s == status),
1424 ) {
1425 let fields = [
1426 ("status", Value::Int(i64::from(status))),
1427 ("title", Value::Text(error::title(status))),
1428 ("message", Value::Text(error::message(status))),
1429 ("reference", Value::Text(&reference)),
1430 ];
1431 let view = Fields(&fields);
1432 let mut out = Output::new(&mut body, self.shared.page_limit);
1433 if engine.render(t, &view, &mut out).is_ok() {
1434 done = true;
1435 } else {
1436 body.clear();
1437 }
1438 }
1439 if !done {
1440 error::builtin_page(status, &reference, &mut body);
1441 }
1442 reply
1443 .status(status)
1444 .header("content-type", "text/html; charset=utf-8")
1445 .header("x-content-type-options", "nosniff")
1446 .header("cache-control", "no-store")
1447 .body(&body);
1448 self.secure(reply, at.tls, |_| false);
1449 }
1450
1451 fn secure(&self, reply: &mut HttpReply, tls: bool, has: impl Fn(&str) -> bool) {
1454 for (name, value) in self.shared.security.fields(tls) {
1455 if !has(name) {
1456 reply.header(name, value.as_bytes());
1457 }
1458 }
1459 }
1460
1461 fn tls(&self, env: &Envelope, authority: Option<&str>) -> bool {
1464 if env.client().scheme() != Scheme::Https {
1465 return false;
1466 }
1467 let a = authority.unwrap_or("").to_ascii_lowercase();
1468 !(self.site.development && is_dev_host(split_port(a.trim_end_matches('.')).0))
1469 }
1470}
1471
1472impl<G: Gate> HttpService for App<G> {
1473 fn call(&self, request: &HttpRequest<'_>, env: &Envelope, reply: &mut HttpReply) -> Outcome {
1474 self.respond(request, env, reply)
1475 }
1476}
1477
1478impl<G: Gate> fmt::Debug for App<G> {
1479 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1480 f.debug_struct("App")
1481 .field("site", &self.site)
1482 .field("base", &self.shared.base)
1483 .field("routes", &self.entries.len())
1484 .finish_non_exhaustive()
1485 }
1486}
1487
1488fn encode_component(s: &str, out: &mut String) {
1490 const HEX: &[u8; 16] = b"0123456789ABCDEF";
1491 for b in s.bytes() {
1492 if b.is_ascii_alphanumeric() || matches!(b, b'-' | b'.' | b'_' | b'~' | b'/') {
1493 out.push(b as char);
1494 } else {
1495 out.push('%');
1496 out.push(HEX[(b >> 4) as usize] as char);
1497 out.push(HEX[(b & 15) as usize] as char);
1498 }
1499 }
1500}
1501
1502#[cfg(test)]
1503mod tests {
1504 use super::*;
1505
1506 #[test]
1507 fn test_split_port() {
1508 assert_eq!(split_port("a.test:80"), ("a.test", Some("80")));
1509 assert_eq!(split_port("a.test"), ("a.test", None));
1510 assert_eq!(split_port("[::1]:8080"), ("[::1]", Some("8080")));
1511 assert_eq!(split_port("[::1]"), ("[::1]", None));
1512 }
1513
1514 #[test]
1515 fn test_valid_host() {
1516 for ok in ["example.com", "localhost:8080", "[::1]", "127.0.0.1:80"] {
1517 assert!(valid_host(ok), "{ok}");
1518 }
1519 for bad in ["", "a b", "a:", "a:x", "[zz]", "a/b"] {
1520 assert!(!valid_host(bad), "{bad}");
1521 }
1522 }
1523
1524 #[test]
1525 fn test_selector() {
1526 assert!(Routes::prefix("/blog").matches("/blog", None));
1527 assert!(Routes::prefix("/blog/").matches("/blog/:slug", None));
1528 assert!(!Routes::prefix("/blog").matches("/blogger", None));
1529 assert!(Routes::named(&["a"]).matches("/x", Some("a")));
1530 assert!(!Routes::named(&["a"]).matches("/x", None));
1531 }
1532}