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, ErrorPage, Event, EventKind, Events, Gate, Identity, NoEvents,
28 NoRender, Open, Output, Render, Request, RequestId, Result, Stage, TemplateId,
29};
30
31pub const DEFAULT_PAGE_LIMIT: usize = 8 << 20;
33
34#[non_exhaustive]
37#[derive(Clone, Copy, Debug, PartialEq, Eq)]
38pub enum Hook {
39 Routed,
42 Finish,
45 BeforeRender,
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
95pub(crate) type HookFn<I, E> = Arc<dyn Fn(&mut Cx<'_, I, E>) -> Result + Send + Sync>;
96
97#[non_exhaustive]
99#[derive(Clone, Copy, Debug, PartialEq, Eq)]
100pub struct Classified {
101 pub route: Option<RouteId>,
103 pub work: WorkClass,
105}
106
107#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
110pub struct RouteId(pub u32);
111
112pub(crate) struct Shared<E: Render> {
114 pub(crate) render: Option<E>,
115 pub(crate) checkpoint: Option<Box<dyn crate::Checkpoint>>,
116 pub(crate) events: Box<dyn Events>,
117 pub(crate) page_limit: usize,
118 pub(crate) base: Box<str>,
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 compression: Option<Compression>,
126}
127
128#[derive(Clone, Copy)]
131pub(crate) struct At<'a> {
132 pub(crate) env: &'a Envelope,
133 pub(crate) tls: bool,
134 pub(crate) json: bool,
136}
137
138type ReadyFn = Arc<dyn Fn() -> bool + Send + Sync>;
139
140#[derive(Clone)]
141enum Health {
142 Live,
143 Ready(ReadyFn),
144}
145
146#[derive(Clone, Debug, Default)]
147struct Site {
148 hosts: Vec<Box<str>>,
150 aliases: Vec<Box<str>>,
151 development: bool,
152 slash: TrailingSlash,
153 lowercase: bool,
154 https_only: bool,
155}
156
157struct Entry<G: Gate, E: Render> {
158 handler: BoxHandler<G::Identity, E>,
159 access: Access<G::Rule>,
160 names: Box<[Box<str>]>,
161 pattern: Box<str>,
162 work: WorkClass,
163 template: Option<TemplateId>,
164 csrf: bool,
165 limits: form_net::Limits,
166 cors: Option<Arc<Cors>>,
167 cache: CachePolicy,
168 cache_values: (Option<Box<str>>, Option<Box<str>>),
170 routed: Box<[HookFn<G::Identity, E>]>,
171 finish: Box<[HookFn<G::Identity, E>]>,
172 before_render: Box<[HookFn<G::Identity, E>]>,
173}
174
175struct Endpoint {
176 methods: [Option<u32>; 10],
178 allow: Box<str>,
179 set: MethodSet,
180}
181
182pub struct App<G: Gate = Open, E: Render = NoRender> {
211 gate: G,
212 shared: Shared<E>,
213 site: Site,
214 trie: Trie,
215 endpoints: Vec<Endpoint>,
216 entries: Vec<Entry<G, E>>,
217 allow_all: Box<str>,
218 assets: Vec<Assets>,
219 health: Vec<(Box<str>, Health)>,
220}
221
222impl App<Open> {
223 #[must_use]
226 pub fn builder() -> Builder<Open> {
227 Builder::new(Open)
228 }
229}
230
231impl<G: Gate> App<G> {
232 #[must_use]
234 pub fn with_gate(gate: G) -> Builder<G> {
235 Builder::new(gate)
236 }
237}
238
239impl<G: Gate, E: Render> App<G, E> {
240 #[must_use]
246 pub fn with(gate: G, engine: E) -> Builder<G, E> {
247 let mut b = Builder::new(gate);
248 b.render = Some(engine);
249 b
250 }
251}
252
253pub struct Builder<G: Gate, E: Render = NoRender> {
256 gate: G,
257 routes: Vec<Route<G::Identity, G::Rule, E>>,
258 hooks: Vec<(Hook, Routes, HookFn<G::Identity, E>)>,
259 site: Site,
260 base: String,
261 render: Option<E>,
262 checkpoint: Option<Box<dyn crate::Checkpoint>>,
263 events: Box<dyn Events>,
264 page_limit: usize,
265 sign_in: Option<String>,
266 challenge: String,
267 wipe: bool,
268 security: SecurityHeaders,
269 assets: Vec<Assets>,
270 health: Vec<(String, Health)>,
271 csrf: Option<csrf_sec::Csrf>,
272 redirect_hosts: Vec<Box<str>>,
273 compression: Option<Compression>,
274 early: Vec<Problem>,
276}
277
278impl<G: Gate, E: Render> Builder<G, E> {
279 fn new(gate: G) -> Self {
280 Self {
281 gate,
282 routes: Vec::new(),
283 hooks: Vec::new(),
284 site: Site::default(),
285 base: String::new(),
286 render: None,
287 checkpoint: None,
288 events: Box::new(NoEvents),
289 page_limit: DEFAULT_PAGE_LIMIT,
290 sign_in: None,
291 challenge: "Session".into(),
292 wipe: false,
293 security: SecurityHeaders::default(),
294 assets: Vec::new(),
295 health: Vec::new(),
296 csrf: None,
297 redirect_hosts: Vec::new(),
298 compression: None,
299 early: Vec::new(),
300 }
301 }
302
303 #[must_use]
307 pub fn compress(mut self, compression: Compression) -> Self {
308 self.compression = Some(compression);
309 self
310 }
311
312 #[must_use]
317 pub fn csrf(mut self, csrf: csrf_sec::Csrf) -> Self {
318 self.csrf = Some(csrf);
319 self
320 }
321
322 #[must_use]
325 pub fn redirect_hosts(mut self, hosts: &[&str]) -> Self {
326 self.redirect_hosts.extend(
327 hosts
328 .iter()
329 .map(|h| h.to_ascii_lowercase().into_boxed_str()),
330 );
331 self
332 }
333
334 #[must_use]
337 pub fn assets(mut self, assets: Assets) -> Self {
338 self.assets.push(assets);
339 self
340 }
341
342 #[must_use]
345 pub fn health(mut self, path: &str) -> Self {
346 self.health.push((path.to_owned(), Health::Live));
347 self
348 }
349
350 #[must_use]
354 pub fn ready(mut self, path: &str, check: impl Fn() -> bool + Send + Sync + 'static) -> Self {
355 self.health
356 .push((path.to_owned(), Health::Ready(Arc::new(check))));
357 self
358 }
359
360 #[must_use]
363 pub fn security_headers(mut self, policy: SecurityHeaders) -> Self {
364 self.security = policy;
365 self
366 }
367
368 #[must_use]
370 pub fn route(mut self, route: Route<G::Identity, G::Rule, E>) -> Self {
371 self.routes.push(route);
372 self
373 }
374
375 #[must_use]
377 pub fn group(mut self, group: Group<G::Identity, G::Rule, E>) -> Self {
378 self.routes.extend(group.into_routes());
379 self
380 }
381
382 #[must_use]
384 pub fn hook<F>(mut self, hook: Hook, routes: Routes, f: F) -> Self
385 where
386 F: Fn(&mut Cx<'_, G::Identity, E>) -> Result + Send + Sync + 'static,
387 {
388 self.hooks.push((hook, routes, Arc::new(f)));
389 self
390 }
391
392 #[must_use]
396 pub fn host(mut self, host: &str) -> Self {
397 self.site.hosts.push(host.to_ascii_lowercase().into());
398 self
399 }
400
401 #[must_use]
404 pub fn alias(mut self, host: &str) -> Self {
405 self.site.aliases.push(host.to_ascii_lowercase().into());
406 self
407 }
408
409 #[must_use]
414 pub fn development(mut self) -> Self {
415 self.site.development = true;
416 self
417 }
418
419 #[must_use]
423 pub fn base(mut self, base: &str) -> Self {
424 self.base = base.to_owned();
425 self
426 }
427
428 #[must_use]
430 pub fn trailing_slash(mut self, policy: TrailingSlash) -> Self {
431 self.site.slash = policy;
432 self
433 }
434
435 #[must_use]
437 pub fn lowercase_paths(mut self) -> Self {
438 self.site.lowercase = true;
439 self
440 }
441
442 #[must_use]
445 pub fn https_only(mut self) -> Self {
446 self.site.https_only = true;
447 self
448 }
449
450 #[must_use]
454 pub fn sign_in(mut self, path: &str) -> Self {
455 self.sign_in = Some(path.to_owned());
456 self
457 }
458
459 #[must_use]
462 pub fn challenge(mut self, challenge: &str) -> Self {
463 challenge.clone_into(&mut self.challenge);
464 self
465 }
466
467 #[must_use]
469 pub fn events(mut self, events: impl Events) -> Self {
470 self.events = Box::new(events);
471 self
472 }
473
474 #[must_use]
476 pub fn checkpoint(mut self, checkpoint: impl crate::Checkpoint) -> Self {
477 self.checkpoint = Some(Box::new(checkpoint));
478 self
479 }
480
481 #[must_use]
483 pub fn page_limit(mut self, bytes: usize) -> Self {
484 self.page_limit = bytes;
485 self
486 }
487
488 #[must_use]
492 pub fn wipe_buffers(mut self) -> Self {
493 self.wipe = true;
494 self
495 }
496
497 pub fn build(self) -> std::result::Result<App<G, E>, BuildError> {
505 let mut problems = self.early;
506 let mut problem = |subject: &str, message: String| {
507 problems.push(Problem {
508 subject: subject.to_owned(),
509 message,
510 });
511 };
512
513 let base = self.base.trim_end_matches('/');
515 if !base.is_empty() {
516 match url::normalize(base, self.site.lowercase) {
517 Ok(n) if n == base && !base.contains(['?', '#']) => {}
518 _ => problem(
519 "base",
520 format!("'{}' is not a canonical path such as '/blog'", self.base),
521 ),
522 }
523 }
524 for h in self.site.hosts.iter().chain(&self.site.aliases) {
525 if !valid_host(h) {
526 problem("host", format!("'{h}' is not a host name or address"));
527 }
528 }
529 if self.site.https_only && self.site.hosts.is_empty() {
530 problem(
531 "https_only",
532 "redirecting to HTTPS needs a declared host; add .host(\"example.com\")".into(),
533 );
534 }
535 if let Some(s) = &self.sign_in {
536 if !is_local_target(s) {
537 problem("sign_in", format!("'{s}' is not a site-relative path"));
538 }
539 }
540 if self.challenge.is_empty() || !self.challenge.bytes().all(|b| (0x20..0x7f).contains(&b)) {
541 problem("challenge", "the challenge must be visible ASCII".into());
542 }
543
544 if !self.security.valid() {
545 problem(
546 "security_headers",
547 "every header value must be visible ASCII and not empty".into(),
548 );
549 }
550 let mut assets = self.assets;
551 for a in &mut assets {
552 let subject = format!("assets {}", a.mount);
553 if a.mount.is_empty()
554 || !is_local_target(&a.mount)
555 || url::normalize(&a.mount, self.site.lowercase).map_or(true, |n| n != a.mount)
556 {
557 problem(
558 &subject,
559 "the mount must be a canonical path such as '/static'".into(),
560 );
561 }
562 match std::fs::canonicalize(&a.dir) {
563 Ok(root) if root.is_dir() => a.root = root,
564 _ => problem(
565 &subject,
566 format!("'{}' is not a folder that exists", a.dir.display()),
567 ),
568 }
569 for r in &self.routes {
570 if a.strip(&r.pattern).is_some() {
571 problem(
572 &r.pattern,
573 format!(
574 "the route is under the assets mount '{}' and can never run",
575 a.mount
576 ),
577 );
578 }
579 }
580 }
581 for h in &self.redirect_hosts {
582 if !valid_host(h) {
583 problem(
584 "redirect_hosts",
585 format!("'{h}' is not a host name or address"),
586 );
587 }
588 }
589 let mut health = Vec::new();
590 for (p, h) in self.health {
591 if !is_local_target(&p) || url::normalize(&p, false).map_or(true, |n| n != p) {
592 problem(
593 "health",
594 format!("'{p}' is not a canonical site-relative path"),
595 );
596 }
597 health.push((p.into_boxed_str(), h));
598 }
599
600 let mut trie = Trie::default();
602 let mut endpoints: Vec<Endpoint> = Vec::new();
603 let mut entries: Vec<Entry<G, E>> = Vec::new();
604 let mut all = MethodSet::EMPTY;
605 for r in self.routes {
606 let parts = match router::parse(&r.pattern) {
607 Ok(p) => p,
608 Err(e) => {
609 problem(&r.pattern, e.message());
610 continue;
611 }
612 };
613 if r.handlers.is_empty() {
614 problem(&r.pattern, "the route has no handler".into());
615 continue;
616 }
617 if matches!(r.access, Access::Undeclared) {
618 problem(
619 &r.pattern,
620 "the route declares no access; add .public() or .allow(rule)".into(),
621 );
622 continue;
623 }
624 let template = match (r.template, &self.render) {
625 (None, _) => None,
626 (Some(t), None) => {
627 problem(
628 &r.pattern,
629 format!("template '{t}' is declared but no render engine is set"),
630 );
631 None
632 }
633 (Some(t), Some(engine)) => {
634 let id = engine.template(t);
635 if id.is_none() {
636 problem(&r.pattern, format!("the engine has no template '{t}'"));
637 }
638 id
639 }
640 };
641 let ep = trie.insert(&parts, endpoints.len() as u32) as usize;
642 if ep == endpoints.len() {
643 endpoints.push(Endpoint {
644 methods: [None; 10],
645 allow: Box::from(""),
646 set: MethodSet::EMPTY,
647 });
648 }
649 let names: Box<[Box<str>]> = router::names(&parts).into();
650 if r.csrf_exempt.is_some_and(|reason| reason.trim().is_empty()) {
651 problem(&r.pattern, "a CSRF exemption needs its reason".into());
652 }
653 if let Some(why) = r.cors.as_deref().and_then(Cors::problem) {
654 problem(&r.pattern, why.into());
655 }
656 if let Err(e) = r.form_limits.validate() {
657 problem(&r.pattern, format!("the form limits are inconsistent: {e}"));
658 }
659 for (m, h) in r.handlers {
660 let subject = format!("{} {}", m.as_str().unwrap_or("?"), r.pattern);
661 if m == Method::Other || m == Method::Connect || m == Method::Trace {
662 problem(&subject, "this method cannot be routed".into());
663 continue;
664 }
665 let slot = &mut endpoints[ep].methods[m.index() as usize];
666 if slot.is_some() {
667 problem(
668 &subject,
669 "a handler for this method and path exists already".into(),
670 );
671 continue;
672 }
673 *slot = Some(entries.len() as u32);
674 let csrf = r.csrf_exempt.is_none() && !m.is_safe();
675 if csrf && self.csrf.is_none() {
676 problem(
677 &subject,
678 "an unsafe method needs CSRF protection; set Builder::csrf(..) or mark the route csrf_exempt(\"reason\")".into(),
679 );
680 }
681 let pick = |at: Hook| -> Box<[HookFn<G::Identity, E>]> {
682 self.hooks
683 .iter()
684 .filter(|(h, sel, _)| *h == at && sel.matches(&r.pattern, r.name))
685 .map(|(_, _, f)| Arc::clone(f))
686 .collect()
687 };
688 entries.push(Entry {
689 handler: h,
690 access: r.access.clone(),
691 names: names.clone(),
692 pattern: r.pattern.as_str().into(),
693 work: r.work,
694 template,
695 csrf,
696 limits: r.form_limits,
697 cors: r.cors.clone(),
698 cache: r.cache,
699 cache_values: (
700 r.cache.value(true).map(String::into_boxed_str),
701 r.cache.value(false).map(String::into_boxed_str),
702 ),
703 routed: pick(Hook::Routed),
704 finish: pick(Hook::Finish),
705 before_render: pick(Hook::BeforeRender),
706 });
707 }
708 }
709 for e in &mut endpoints {
710 let mut set = MethodSet::EMPTY;
711 for m in [
712 Method::Get,
713 Method::Head,
714 Method::Post,
715 Method::Put,
716 Method::Delete,
717 Method::Options,
718 Method::Patch,
719 ] {
720 if e.methods[m.index() as usize].is_some() {
721 set = set.with(m);
722 }
723 }
724 if set.contains(Method::Get) {
725 set = set.with(Method::Head);
726 }
727 set = set.with(Method::Options);
728 e.set = set;
729 all = all.union(set);
730 let mut v = Vec::new();
731 set.write_allow(&mut v);
732 e.allow = String::from_utf8(v).unwrap_or_default().into();
733 }
734 if !problems.is_empty() {
735 return Err(BuildError::new(problems));
736 }
737 let mut v = Vec::new();
738 all.with(Method::Options).write_allow(&mut v);
739 Ok(App {
740 gate: self.gate,
741 shared: Shared {
742 render: self.render,
743 checkpoint: self.checkpoint,
744 events: self.events,
745 page_limit: self.page_limit,
746 base: base.into(),
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> Builder<G, NoRender> {
767 #[must_use]
771 pub fn render<E: Render>(self, engine: E) -> Builder<G, E> {
772 let mut b = Builder::new(self.gate);
773 if !self.routes.is_empty() || !self.hooks.is_empty() {
774 b.early.push(Problem {
775 subject: "render".into(),
776 message:
777 "set the template engine before adding routes and hooks (or use App::with)"
778 .into(),
779 });
780 }
781 b.render = Some(engine);
782 b.site = self.site;
783 b.base = self.base;
784 b.checkpoint = self.checkpoint;
785 b.events = self.events;
786 b.page_limit = self.page_limit;
787 b.sign_in = self.sign_in;
788 b.challenge = self.challenge;
789 b.wipe = self.wipe;
790 b.security = self.security;
791 b.assets = self.assets;
792 b.health = self.health;
793 b.csrf = self.csrf;
794 b.redirect_hosts = self.redirect_hosts;
795 b.compression = self.compression;
796 b
797 }
798}
799
800impl<G: Gate, E: Render> fmt::Debug for Builder<G, E> {
801 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
802 f.debug_struct("Builder")
803 .field("routes", &self.routes)
804 .field("site", &self.site)
805 .field("base", &self.base)
806 .finish_non_exhaustive()
807 }
808}
809
810fn valid_host(h: &str) -> bool {
811 let (host, port) = split_port(h);
812 let host_ok = if let Some(inner) = host.strip_prefix('[').and_then(|r| r.strip_suffix(']')) {
813 inner.parse::<std::net::Ipv6Addr>().is_ok()
814 } else {
815 !host.is_empty()
816 && host.len() <= 253
817 && host
818 .bytes()
819 .all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'.')
820 };
821 host_ok && port.is_none_or(|p| !p.is_empty() && p.parse::<u16>().is_ok())
822}
823
824fn split_port(a: &str) -> (&str, Option<&str>) {
827 if a.starts_with('[') {
828 return match a.find(']') {
829 Some(i) => match a[i + 1..].strip_prefix(':') {
830 Some(p) => (&a[..=i], Some(p)),
831 None => (a, None),
832 },
833 None => (a, None),
834 };
835 }
836 match a.rsplit_once(':') {
837 Some((h, p)) => (h, Some(p)),
838 None => (a, None),
839 }
840}
841
842fn is_dev_host(host: &str) -> bool {
844 const SUFFIX: &[u8] = b".localhost";
845 let b = host.as_bytes();
846 ["localhost", "127.0.0.1", "[::1]"]
847 .iter()
848 .any(|d| d.eq_ignore_ascii_case(host))
849 || (b.len() > SUFFIX.len() && b[b.len() - SUFFIX.len()..].eq_ignore_ascii_case(SUFFIX))
850}
851
852fn host_parts(authority: &str) -> (&str, &str) {
854 let full = authority.trim_end_matches('.');
855 (full, split_port(full).0.trim_end_matches('.'))
856}
857
858fn host_listed(list: &[Box<str>], full: &str, host: &str) -> bool {
861 list.iter().any(|h| {
862 h.eq_ignore_ascii_case(full) || (split_port(h).1.is_none() && h.eq_ignore_ascii_case(host))
863 })
864}
865
866thread_local! {
867 static SCRATCH: Cell<Option<Box<Out>>> = const { Cell::new(None) };
871}
872
873const KEEP_CAPACITY: usize = 1 << 20;
875
876enum Placed<'a> {
878 Here(Cow<'a, str>),
879 Redirect(String),
880 Refuse(u16, &'static str, Option<url::UrlError>),
881}
882
883impl<G: Gate, E: Render> App<G, E> {
884 pub fn respond(&self, req: &HttpRequest<'_>, env: &Envelope, reply: &mut HttpReply) -> Outcome {
886 self.serve(req, env, None, reply).outcome
887 }
888
889 pub fn serve(
894 &self,
895 req: &HttpRequest<'_>,
896 env: &Envelope,
897 who: Option<&G::Identity>,
898 reply: &mut HttpReply,
899 ) -> Served {
900 let mut report = Served {
901 outcome: Outcome::Done,
902 route: None,
903 cache: Cacheable::No,
904 };
905 report.outcome = self.run(req, env, who, reply, &mut report);
906 report
907 }
908
909 pub fn respond_identified(
914 &self,
915 req: &HttpRequest<'_>,
916 env: &Envelope,
917 who: &G::Identity,
918 reply: &mut HttpReply,
919 ) -> Outcome {
920 self.serve(req, env, Some(who), reply).outcome
921 }
922
923 #[must_use]
926 pub fn classify(&self, head: &HttpHead<'_>) -> Classified {
927 let none = Classified {
928 route: None,
929 work: WorkClass::Quick,
930 };
931 let Placed::Here(path) = self.place(head.authority(), head.path(), Scheme::Https, true)
932 else {
933 return none;
934 };
935 let Some((ep, _)) = self.trie.find(&path) else {
936 return none;
937 };
938 match self.entry_for(ep as usize, head.method()) {
939 Some(i) => Classified {
940 route: Some(RouteId(i)),
941 work: self.entries[i as usize].work,
942 },
943 None => none,
944 }
945 }
946
947 #[must_use]
949 pub fn gate(&self) -> &G {
950 &self.gate
951 }
952
953 fn entry_for(&self, ep: usize, m: Method) -> Option<u32> {
954 let methods = &self.endpoints[ep].methods;
955 match m {
956 Method::Other => None,
957 Method::Head => {
958 methods[Method::Head.index() as usize].or(methods[Method::Get.index() as usize])
959 }
960 m => methods[m.index() as usize],
961 }
962 }
963
964 fn place<'a>(
967 &self,
968 authority: Option<&'a str>,
969 raw: &'a str,
970 scheme: Scheme,
971 quiet: bool,
972 ) -> Placed<'a> {
973 let site = &self.site;
974 let authority = authority.unwrap_or("");
975 let (full, host_only) = host_parts(authority);
976 let listed = |list: &[Box<str>]| host_listed(list, full, host_only);
977 let dev = site.development && is_dev_host(host_only);
978 let mut target_host: Option<&str> = None;
979 if !site.hosts.is_empty() && !dev && !listed(&site.hosts) {
982 if listed(&site.aliases) {
983 target_host = site.hosts.first().map(AsRef::as_ref);
984 } else {
985 return Placed::Refuse(421, "host.unknown", None);
986 }
987 }
988 let mut https = scheme == Scheme::Https;
989 if site.https_only && !https && !dev {
990 https = true;
991 target_host = target_host.or(site.hosts.first().map(AsRef::as_ref));
992 }
993
994 let canon = match url::normalize(raw, site.lowercase) {
996 Ok(c) => c,
997 Err(e) => return Placed::Refuse(400, e.code(), Some(e)),
998 };
999 let base = self.shared.base.as_ref();
1000 let c: &str = &canon;
1001 let rel_start = if base.is_empty() {
1002 0
1003 } else if c == base {
1004 c.len()
1005 } else if c.strip_prefix(base).is_some_and(|r| r.starts_with('/')) {
1006 base.len()
1007 } else {
1008 return Placed::Refuse(404, "site.outside_base", None);
1009 };
1010 let rel = &c[rel_start..];
1011 let mut fixed: Option<String> = None;
1012 let health = self.health.iter().any(|(p, _)| p.as_ref() == rel);
1013 if rel.is_empty() {
1014 fixed = Some("/".into());
1015 } else if rel.len() > 1 && !health {
1016 match site.slash {
1017 TrailingSlash::Strip => {
1018 if let Some(s) = rel.strip_suffix('/') {
1019 fixed = Some(s.to_owned());
1020 }
1021 }
1022 TrailingSlash::Add => {
1023 let trimmed = rel.strip_suffix('/').unwrap_or(rel);
1024 let file_like = trimmed.rsplit('/').next().unwrap_or("").contains('.');
1025 if file_like && rel.ends_with('/') {
1026 fixed = Some(trimmed.to_owned());
1028 } else if !file_like && !rel.ends_with('/') {
1029 fixed = Some(format!("{rel}/"));
1030 }
1031 }
1032 TrailingSlash::Ignore => {}
1033 }
1034 }
1035 if target_host.is_none() && fixed.is_none() {
1036 if let Cow::Borrowed(c) = canon {
1037 let mut path = &c[rel_start..];
1038 if site.slash == TrailingSlash::Ignore && path.len() > 1 {
1039 path = path.strip_suffix('/').unwrap_or(path);
1040 }
1041 return Placed::Here(Cow::Borrowed(path));
1042 }
1043 }
1044 if quiet {
1045 return Placed::Refuse(0, "", None);
1046 }
1047 let rel_out = fixed.as_deref().unwrap_or(rel);
1048 let mut loc = String::with_capacity(raw.len() + 32);
1049 if let Some(h) = target_host {
1050 loc.push_str(if https { "https://" } else { "http://" });
1051 loc.push_str(h);
1052 }
1053 loc.push_str(base);
1054 loc.push_str(rel_out);
1055 Placed::Redirect(loc)
1056 }
1057
1058 fn run(
1059 &self,
1060 http: &HttpRequest<'_>,
1061 env: &Envelope,
1062 who: Option<&G::Identity>,
1063 reply: &mut HttpReply,
1064 report: &mut Served,
1065 ) -> Outcome {
1066 let method = http.method();
1067 let raw_path = http.path();
1068 let authority = http.head().authority();
1069 let at = At {
1070 env,
1071 tls: self.tls(env, authority),
1072 json: wants_json(http.header("accept")),
1073 };
1074 if raw_path == "*" {
1075 if self.claim(authority, &self.shared.base).is_none() {
1078 self.refuse(
1079 421,
1080 "host.unknown",
1081 EventKind::Refused,
1082 None,
1083 None,
1084 at,
1085 reply,
1086 );
1087 return Outcome::Done;
1088 }
1089 if method == Method::Options {
1090 reply.status(204).header("allow", self.allow_all.as_bytes());
1091 self.secure(reply, at.tls, |_| false);
1092 } else {
1093 self.refuse(
1094 400,
1095 "url.asterisk",
1096 EventKind::Refused,
1097 None,
1098 None,
1099 at,
1100 reply,
1101 );
1102 }
1103 return Outcome::Done;
1104 }
1105 let host_raw = authority.unwrap_or("");
1106 let placed = self.place(authority, raw_path, env.client().scheme(), false);
1107 let path = match placed {
1108 Placed::Here(path) => path,
1109 Placed::Redirect(mut loc) => {
1110 if let Some(q) = http.query() {
1111 loc.push('?');
1112 loc.push_str(q);
1113 }
1114 let status = if matches!(method, Method::Get | Method::Head) {
1115 301
1116 } else {
1117 308
1118 };
1119 reply.status(status).header("location", loc.as_bytes());
1120 self.secure(reply, at.tls, |_| false);
1121 return Outcome::Done;
1122 }
1123 Placed::Refuse(status, code, src) => {
1124 let src = src
1125 .as_ref()
1126 .map(|e| e as &(dyn std::error::Error + 'static));
1127 self.refuse(status, code, EventKind::Refused, None, src, at, reply);
1128 return Outcome::Done;
1129 }
1130 };
1131 if let Some(outcome) = self.early(http, at, &path, reply) {
1133 return outcome;
1134 }
1135 if let Some(outcome) = self.preflight(http, at, &path, reply) {
1137 return outcome;
1138 }
1139
1140 let req = Request {
1141 http,
1142 env,
1143 path: &path,
1144 host: host_raw,
1145 route: None,
1146 tls: at.tls,
1147 json: at.json,
1148 };
1149
1150 let owned;
1152 let owned_here = who.is_none();
1153 let who: &G::Identity = match who {
1154 Some(w) => w,
1155 None => match self.gate.authenticate(&req) {
1156 Ok(w) => {
1157 owned = w;
1158 &owned
1159 }
1160 Err(d) => {
1161 self.deny(d, &req, reply);
1162 return Outcome::Done;
1163 }
1164 },
1165 };
1166
1167 let mut gate_cookie = if owned_here { who.set_cookie() } else { None };
1168 let outcome = self.routed(http, env, at, req, who, &mut gate_cookie, reply, report);
1169 if let Some(c) = gate_cookie {
1175 reply.header("set-cookie", c.as_bytes());
1176 report.cache = Cacheable::No;
1177 }
1178 outcome
1179 }
1180
1181 #[allow(clippy::too_many_arguments)]
1183 fn routed<'r>(
1184 &'r self,
1185 http: &HttpRequest<'_>,
1186 env: &Envelope,
1187 at: At<'_>,
1188 mut req: Request<'r>,
1189 who: &G::Identity,
1190 gate_cookie: &mut Option<&str>,
1191 reply: &mut HttpReply,
1192 report: &mut Served,
1193 ) -> Outcome {
1194 let method = http.method();
1195 let path = req.path;
1196 let host_raw = req.host;
1197 let Some((ep, caps)) = self.trie.find(path) else {
1199 self.refuse(404, "route.none", EventKind::Refused, None, None, at, reply);
1200 return Outcome::Done;
1201 };
1202 let endpoint = &self.endpoints[ep as usize];
1203 let Some(idx) = self.entry_for(ep as usize, method) else {
1204 if method == Method::Options {
1205 reply.status(204).header("allow", endpoint.allow.as_bytes());
1206 self.secure(reply, at.tls, |_| false);
1207 return Outcome::Done;
1208 }
1209 let status = if method == Method::Other { 501 } else { 405 };
1210 let code = if status == 501 {
1211 "method.unknown"
1212 } else {
1213 "method.not_allowed"
1214 };
1215 self.refuse(status, code, EventKind::Refused, None, None, at, reply);
1216 if status == 405 {
1217 reply.header("allow", endpoint.allow.as_bytes());
1218 }
1219 return Outcome::Done;
1220 };
1221 let entry = &self.entries[idx as usize];
1222 req.route = Some(&entry.pattern);
1223 report.route = Some(RouteId(idx));
1224
1225 if let Access::Rule(rule) = &entry.access {
1227 if let Err(d) = self.gate.authorize(who, rule, &req) {
1228 self.deny(d, &req, reply);
1229 return Outcome::Done;
1230 }
1231 }
1232 if let Some(cp) = &self.shared.checkpoint {
1233 match cp.check(env, Stage::Authorized) {
1234 Verdict::Pass => {}
1235 Verdict::Deny { status } => {
1236 self.refuse(
1237 status,
1238 "checkpoint.denied",
1239 EventKind::Revoked,
1240 req.route,
1241 None,
1242 at,
1243 reply,
1244 );
1245 return Outcome::Done;
1246 }
1247 _ => {
1248 self.refuse(
1249 403,
1250 "checkpoint.killed",
1251 EventKind::Revoked,
1252 req.route,
1253 None,
1254 at,
1255 reply,
1256 );
1257 return Outcome::Close;
1258 }
1259 }
1260 }
1261
1262 let binding = who.csrf_binding();
1264 match (&self.shared.csrf, entry.csrf) {
1265 (Some(engine), true) => {
1266 let scheme = if env.client().scheme() == Scheme::Https {
1267 "https://"
1268 } else {
1269 "http://"
1270 };
1271 let own = format!("{scheme}{}", host_raw.to_ascii_lowercase());
1272 match csrf::check(engine, http, binding, &own, &entry.limits) {
1273 Ok(()) => {}
1274 Err(code) => {
1275 self.refuse(403, code, EventKind::Denied, req.route, None, at, reply);
1276 return Outcome::Done;
1277 }
1278 }
1279 }
1280 (None, true) => {
1282 self.refuse(
1283 500,
1284 "csrf.no_engine",
1285 EventKind::Failed,
1286 req.route,
1287 None,
1288 at,
1289 reply,
1290 );
1291 return Outcome::Done;
1292 }
1293 (_, false) => {}
1294 }
1295
1296 let mut out = SCRATCH.with(Cell::take).unwrap_or_default();
1298 out.reset();
1299 let outcome = self.handle(
1300 entry,
1301 req,
1302 who,
1303 caps,
1304 (gate_cookie, binding),
1305 &mut out,
1306 reply,
1307 &mut report.cache,
1308 );
1309 if self.shared.wipe {
1310 out.wipe();
1311 }
1312 if out.body.capacity() <= KEEP_CAPACITY && out.values.capacity() <= KEEP_CAPACITY {
1313 SCRATCH.with(|s| s.set(Some(out)));
1314 }
1315 outcome
1316 }
1317
1318 fn preflight(
1320 &self,
1321 http: &HttpRequest<'_>,
1322 at: At<'_>,
1323 path: &str,
1324 reply: &mut HttpReply,
1325 ) -> Option<Outcome> {
1326 if http.method() != Method::Options {
1327 return None;
1328 }
1329 let origin = http.header("origin")?;
1330 let wanted = Method::from_bytes(http.header("access-control-request-method")?);
1331 let (ep, _) = self.trie.find(path)?;
1332 let ep = ep as usize;
1333 if self.entry_for(ep, Method::Options).is_some() {
1334 return None;
1335 }
1336 let cors = self
1337 .entry_for(ep, wanted)
1338 .and_then(|i| self.entries[i as usize].cors.as_ref())?;
1339 let mut set = MethodSet::EMPTY;
1341 for m in self.endpoints[ep].set.iter() {
1342 let shares = self.entry_for(ep, m).is_some_and(|i| {
1343 self.entries[i as usize]
1344 .cors
1345 .as_ref()
1346 .is_some_and(|c| Arc::ptr_eq(c, cors))
1347 });
1348 if shares {
1349 set = set.with(m);
1350 }
1351 }
1352 let ok = cors.preflight(
1353 origin,
1354 wanted,
1355 http.header("access-control-request-headers"),
1356 set,
1357 reply,
1358 );
1359 if !ok {
1360 self.event(EventKind::Refused, at.env, None, 204, "cors.denied", None);
1361 }
1362 self.secure(reply, at.tls, |_| false);
1363 Some(Outcome::Done)
1364 }
1365
1366 fn early(
1368 &self,
1369 http: &HttpRequest<'_>,
1370 at: At<'_>,
1371 path: &str,
1372 reply: &mut HttpReply,
1373 ) -> Option<Outcome> {
1374 let method = http.method();
1375 let get_like = matches!(method, Method::Get | Method::Head);
1376 if let Some((_, h)) = self.health.iter().find(|(p, _)| p.as_ref() == path) {
1377 if !get_like {
1378 self.not_get(method, at, reply);
1379 return Some(Outcome::Done);
1380 }
1381 let (status, text) = match h {
1382 Health::Live => (200, "ok"),
1383 Health::Ready(f) if f() => (200, "ready"),
1384 Health::Ready(_) => (503, "not ready"),
1385 };
1386 reply.text(status, text).header("cache-control", "no-store");
1387 self.secure(reply, at.tls, |_| false);
1388 return Some(Outcome::Done);
1389 }
1390 let (assets, rel) = self
1391 .assets
1392 .iter()
1393 .find_map(|a| a.strip(path).map(|rel| (a, rel)))?;
1394 if !get_like {
1395 self.not_get(method, at, reply);
1396 return Some(Outcome::Done);
1397 }
1398 let found = assets::find(assets, rel, http.header("accept-encoding"));
1399 let key = (at.env.connection().get(), at.env.seq().get());
1400 if let Some(tag) = assets::continuing(key) {
1401 return Some(assets::next_part(key, &tag, found.as_ref(), http, reply));
1404 }
1405 let Some(found) = found else {
1406 self.refuse(
1407 404,
1408 "assets.none",
1409 EventKind::Refused,
1410 None,
1411 None,
1412 at,
1413 reply,
1414 );
1415 return Some(Outcome::Done);
1416 };
1417 let Some(outcome) = assets::serve(assets, &found, key, http, reply) else {
1418 self.refuse(
1419 500,
1420 "assets.unreadable",
1421 EventKind::Failed,
1422 None,
1423 None,
1424 at,
1425 reply,
1426 );
1427 return Some(Outcome::Done);
1428 };
1429 self.secure(reply, at.tls, |_| false);
1430 Some(outcome)
1431 }
1432
1433 fn not_get(&self, method: Method, at: At<'_>, reply: &mut HttpReply) {
1435 if method == Method::Options {
1436 reply.status(204).header("allow", "GET, HEAD, OPTIONS");
1437 self.secure(reply, at.tls, |_| false);
1438 } else {
1439 let (status, code) = if method == Method::Other {
1440 (501, "method.unknown")
1441 } else {
1442 (405, "method.not_allowed")
1443 };
1444 self.refuse(status, code, EventKind::Refused, None, None, at, reply);
1445 if status == 405 {
1446 reply.header("allow", "GET, HEAD, OPTIONS");
1447 }
1448 }
1449 }
1450
1451 #[allow(clippy::too_many_arguments)]
1452 fn handle(
1453 &self,
1454 entry: &Entry<G, E>,
1455 req: Request<'_>,
1456 who: &G::Identity,
1457 caps: Captures,
1458 (gate_cookie, binding): (&mut Option<&str>, Option<&[u8]>),
1459 out: &mut Out,
1460 reply: &mut HttpReply,
1461 cacheable: &mut Cacheable,
1462 ) -> Outcome {
1463 let at = At {
1464 env: req.env,
1465 tls: req.tls,
1466 json: req.json,
1467 };
1468 let route = req.route;
1469 let http = req.http;
1470 let guest = who.is_guest();
1471 let result = {
1472 let mut cx = Cx {
1473 req,
1474 who,
1475 caps,
1476 names: &entry.names,
1477 out: &mut *out,
1478 shared: &self.shared,
1479 template: entry.template,
1480 binding,
1481 limits: &entry.limits,
1482 before_render: &entry.before_render,
1483 rendering: false,
1484 };
1485 catch_unwind(AssertUnwindSafe(|| -> Result {
1486 for h in entry.routed.iter() {
1487 h(&mut cx)?;
1488 }
1489 entry.handler.call(&mut cx)?;
1490 for h in entry.finish.iter() {
1491 h(&mut cx)?;
1492 }
1493 Ok(())
1494 }))
1495 };
1496 match result {
1497 Ok(Ok(())) if !out.invalid => {
1498 if gate_cookie.is_some_and(|c| out.sets_cookie(c)) {
1501 *gate_cookie = None;
1502 }
1503 let cookies = gate_cookie.is_some() || out.has("set-cookie");
1508 let ours = !out.has("cache-control");
1509 if cookies {
1510 let public = out
1511 .get("cache-control")
1512 .is_some_and(|v| v.windows(6).any(|w| w.eq_ignore_ascii_case(b"public")));
1513 if ours || public {
1514 out.remove("cache-control");
1515 out.header("cache-control", b"private, no-store");
1516 }
1517 } else if ours {
1518 let (for_guest, for_member) = &entry.cache_values;
1519 let value = if guest { for_guest } else { for_member };
1520 if let Some(v) = value {
1521 out.header("cache-control", v.as_bytes());
1522 }
1523 }
1524 if let Some(c) = self.shared.compression {
1525 compress(c, http.header("accept-encoding"), out);
1526 }
1527 *cacheable = match entry.cache {
1528 CachePolicy::Guests { max_age } | CachePolicy::Public { max_age }
1529 if guest
1530 && ours
1531 && !cookies
1532 && entry.cors.is_none()
1533 && matches!(http.method(), Method::Get | Method::Head)
1534 && out.status == 200
1535 && out
1536 .get("vary")
1537 .is_none_or(|v| v.eq_ignore_ascii_case(b"accept-encoding")) =>
1538 {
1539 Cacheable::Guests { max_age }
1540 }
1541 _ => Cacheable::No,
1542 };
1543 reply.status(out.status);
1544 if let Some(c) = gate_cookie.take() {
1545 reply.header("set-cookie", c.as_bytes());
1546 }
1547 for &(ns, ne, vs, ve) in &out.spans {
1548 let name = core::str::from_utf8(&out.names[ns as usize..ne as usize])
1549 .unwrap_or("x-invalid");
1550 reply.header(name, &out.values[vs as usize..ve as usize]);
1551 }
1552 if !out.has("x-content-type-options") {
1553 reply.header("x-content-type-options", "nosniff");
1554 }
1555 if let Some(cors) = &entry.cors {
1556 cors.actual(http.header("origin"), reply);
1557 }
1558 self.secure(reply, at.tls, |n| out.has(n));
1559 reply.body(&out.body);
1560 Outcome::Done
1561 }
1562 Ok(Ok(())) => {
1563 out.reset();
1564 self.refuse(
1565 500,
1566 "reply.invalid_field",
1567 EventKind::Failed,
1568 route,
1569 None,
1570 at,
1571 reply,
1572 );
1573 Outcome::Done
1574 }
1575 Ok(Err(e)) => {
1576 out.reset();
1577 let src = std::error::Error::source(&e);
1578 self.refuse(
1579 e.status(),
1580 e.code(),
1581 EventKind::Failed,
1582 route,
1583 src,
1584 at,
1585 reply,
1586 );
1587 Outcome::Done
1588 }
1589 Err(_) => {
1590 out.reset();
1591 self.refuse(
1592 500,
1593 "handler.panic",
1594 EventKind::Panicked,
1595 route,
1596 None,
1597 at,
1598 reply,
1599 );
1600 Outcome::Done
1601 }
1602 }
1603 }
1604
1605 fn deny(&self, d: Denial, req: &Request<'_>, reply: &mut HttpReply) {
1607 let env = req.env;
1608 let at = At {
1609 env,
1610 tls: req.tls,
1611 json: req.json,
1612 };
1613 if d.kind() == DenialKind::SignIn {
1614 if let Some(page) = &self.shared.sign_in {
1615 if matches!(req.method(), Method::Get | Method::Head) {
1616 let mut loc = String::with_capacity(64);
1617 loc.push_str(&self.shared.base);
1618 loc.push_str(page);
1619 loc.push_str(if page.contains('?') {
1620 "&next="
1621 } else {
1622 "?next="
1623 });
1624 let mut next = String::new();
1625 next.push_str(&self.shared.base);
1626 next.push_str(req.path);
1627 if let Some(q) = req.query() {
1628 next.push('?');
1629 next.push_str(q);
1630 }
1631 encode_component(&next, &mut loc);
1632 self.event(EventKind::Denied, env, req.route, 303, d.code(), None);
1633 reply.status(303).header("location", loc.as_bytes());
1634 self.secure(reply, at.tls, |_| false);
1635 if let Some(c) = d.cookie() {
1636 reply.header("set-cookie", c.as_bytes());
1637 }
1638 return;
1639 }
1640 }
1641 }
1642 let status = d.status_code();
1643 self.refuse(
1644 status,
1645 d.code(),
1646 EventKind::Denied,
1647 req.route,
1648 None,
1649 at,
1650 reply,
1651 );
1652 if status == 401 {
1653 reply.header("www-authenticate", self.shared.challenge.as_bytes());
1654 }
1655 if let Some(c) = d.cookie() {
1656 reply.header("set-cookie", c.as_bytes());
1657 }
1658 }
1659
1660 fn event(
1661 &self,
1662 kind: EventKind,
1663 env: &Envelope,
1664 route: Option<&str>,
1665 status: u16,
1666 code: &'static str,
1667 source: Option<&(dyn std::error::Error + 'static)>,
1668 ) {
1669 self.shared.events.event(&Event {
1670 kind,
1671 id: RequestId::of(env),
1672 route,
1673 status,
1674 code,
1675 source,
1676 });
1677 }
1678
1679 #[allow(clippy::too_many_arguments)]
1683 fn refuse(
1684 &self,
1685 status: u16,
1686 code: &'static str,
1687 kind: EventKind,
1688 route: Option<&str>,
1689 source: Option<&(dyn std::error::Error + 'static)>,
1690 at: At<'_>,
1691 reply: &mut HttpReply,
1692 ) {
1693 self.event(kind, at.env, route, status, code, source);
1694 let reference = RequestId::of(at.env).to_string();
1695 let mut body = Vec::with_capacity(512);
1696 if at.json {
1697 error::json_body(status, &reference, &mut body);
1698 reply
1699 .status(status)
1700 .header("content-type", "application/json")
1701 .header("x-content-type-options", "nosniff")
1702 .header("cache-control", "no-store")
1703 .body(&body);
1704 self.secure(reply, at.tls, |_| false);
1705 return;
1706 }
1707 let mut done = false;
1708 if let Some(engine) = self.shared.render.as_ref() {
1709 let page = ErrorPage {
1710 status,
1711 title: error::title(status),
1712 message: error::message(status),
1713 reference: &reference,
1714 };
1715 let mut out = Output::new(&mut body, self.shared.page_limit);
1716 let wrote = engine.error_page(&page, &mut out);
1717 if matches!(wrote, Ok(true)) && !out.over() {
1718 done = true;
1719 } else {
1720 body.clear();
1721 }
1722 }
1723 if !done {
1724 error::builtin_page(status, &reference, &mut body);
1725 }
1726 reply
1727 .status(status)
1728 .header("content-type", "text/html; charset=utf-8")
1729 .header("x-content-type-options", "nosniff")
1730 .header("cache-control", "no-store")
1731 .body(&body);
1732 self.secure(reply, at.tls, |_| false);
1733 }
1734
1735 fn secure(&self, reply: &mut HttpReply, tls: bool, has: impl Fn(&str) -> bool) {
1738 for (name, value) in self.shared.security.fields(tls) {
1739 if !has(name) {
1740 reply.header(name, value.as_bytes());
1741 }
1742 }
1743 }
1744
1745 pub(crate) fn claim(&self, authority: Option<&str>, path: &str) -> Option<(u8, usize)> {
1749 let (full, host_only) = host_parts(authority.unwrap_or(""));
1750 let listed = |list: &[Box<str>]| host_listed(list, full, host_only);
1751 let rank = if self.site.hosts.is_empty() {
1752 0
1753 } else if listed(&self.site.hosts) || listed(&self.site.aliases) {
1754 2
1755 } else if self.site.development && is_dev_host(host_only) {
1756 1
1757 } else {
1758 return None;
1759 };
1760 let base = self.shared.base.as_ref();
1761 let inside = base.is_empty()
1762 || path == base
1763 || path.strip_prefix(base).is_some_and(|r| r.starts_with('/'));
1764 inside.then_some((rank, base.len()))
1765 }
1766
1767 fn tls(&self, env: &Envelope, authority: Option<&str>) -> bool {
1770 if env.client().scheme() != Scheme::Https {
1771 return false;
1772 }
1773 let (_, host) = host_parts(authority.unwrap_or(""));
1774 !(self.site.development && is_dev_host(host))
1775 }
1776}
1777
1778impl<G: Gate, E: Render> HttpService for App<G, E> {
1779 fn call(&self, request: &HttpRequest<'_>, env: &Envelope, reply: &mut HttpReply) -> Outcome {
1780 self.respond(request, env, reply)
1781 }
1782}
1783
1784impl<G: Gate, E: Render> fmt::Debug for App<G, E> {
1785 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1786 f.debug_struct("App")
1787 .field("site", &self.site)
1788 .field("base", &self.shared.base)
1789 .field("routes", &self.entries.len())
1790 .finish_non_exhaustive()
1791 }
1792}
1793
1794fn wants_json(accept: Option<&[u8]>) -> bool {
1796 accept.is_some_and(|a| a.windows(4).any(|w| w.eq_ignore_ascii_case(b"json")))
1798 && parse_net::best_media(accept, &["text/html", "application/json"]) == Some(1)
1799}
1800
1801fn compress(c: Compression, accept_encoding: Option<&[u8]>, out: &mut Out) {
1805 if out.status != 200 || out.has("content-encoding") {
1806 return;
1807 }
1808 if !out.get("content-type").is_some_and(compressible) {
1809 return;
1810 }
1811 if !out.has("vary") {
1812 out.header("vary", b"accept-encoding");
1813 }
1814 let accepts = accept_encoding
1815 .and_then(|v| parse_net::best_encoding(Some(v), &["gzip", "identity"]))
1816 == Some(0);
1817 if !accepts || out.body.len() < c.min_bytes {
1818 return;
1819 }
1820 let level = compress_lib::Level::new(c.level).unwrap_or(compress_lib::Level::DEFAULT);
1821 let packed = compress_lib::gzip::compress_with(&out.body, level);
1822 if packed.len() < out.body.len() {
1823 out.body.clear();
1824 out.body.extend_from_slice(&packed);
1825 out.header("content-encoding", b"gzip");
1826 }
1827}
1828
1829fn encode_component(s: &str, out: &mut String) {
1831 const HEX: &[u8; 16] = b"0123456789ABCDEF";
1832 for b in s.bytes() {
1833 if b.is_ascii_alphanumeric() || matches!(b, b'-' | b'.' | b'_' | b'~' | b'/') {
1834 out.push(b as char);
1835 } else {
1836 out.push('%');
1837 out.push(HEX[(b >> 4) as usize] as char);
1838 out.push(HEX[(b & 15) as usize] as char);
1839 }
1840 }
1841}
1842
1843#[cfg(test)]
1844mod tests {
1845 use super::*;
1846
1847 #[test]
1848 fn test_split_port() {
1849 assert_eq!(split_port("a.test:80"), ("a.test", Some("80")));
1850 assert_eq!(split_port("a.test"), ("a.test", None));
1851 assert_eq!(split_port("[::1]:8080"), ("[::1]", Some("8080")));
1852 assert_eq!(split_port("[::1]"), ("[::1]", None));
1853 }
1854
1855 #[test]
1856 fn test_valid_host() {
1857 for ok in ["example.com", "localhost:8080", "[::1]", "127.0.0.1:80"] {
1858 assert!(valid_host(ok), "{ok}");
1859 }
1860 for bad in ["", "a b", "a:", "a:x", "[zz]", "a/b"] {
1861 assert!(!valid_host(bad), "{bad}");
1862 }
1863 }
1864
1865 #[test]
1866 fn test_selector() {
1867 assert!(Routes::prefix("/blog").matches("/blog", None));
1868 assert!(Routes::prefix("/blog/").matches("/blog/:slug", None));
1869 assert!(!Routes::prefix("/blog").matches("/blogger", None));
1870 assert!(Routes::named(&["a"]).matches("/x", Some("a")));
1871 assert!(!Routes::named(&["a"]).matches("/x", None));
1872 }
1873}