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