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