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]
320 pub fn csrf(mut self, csrf: csrf_sec::Csrf) -> Self {
321 self.csrf = Some(csrf);
322 self
323 }
324
325 #[must_use]
328 pub fn redirect_hosts(mut self, hosts: &[&str]) -> Self {
329 self.redirect_hosts.extend(
330 hosts
331 .iter()
332 .map(|h| h.to_ascii_lowercase().into_boxed_str()),
333 );
334 self
335 }
336
337 #[must_use]
340 pub fn assets(mut self, assets: Assets) -> Self {
341 self.assets.push(assets);
342 self
343 }
344
345 #[must_use]
348 pub fn health(mut self, path: &str) -> Self {
349 self.health.push((path.to_owned(), Health::Live));
350 self
351 }
352
353 #[must_use]
357 pub fn ready(mut self, path: &str, check: impl Fn() -> bool + Send + Sync + 'static) -> Self {
358 self.health
359 .push((path.to_owned(), Health::Ready(Arc::new(check))));
360 self
361 }
362
363 #[must_use]
366 pub fn security_headers(mut self, policy: SecurityHeaders) -> Self {
367 self.security = policy;
368 self
369 }
370
371 #[must_use]
373 pub fn route(mut self, route: Route<G::Identity, G::Rule, E>) -> Self {
374 self.routes.push(route);
375 self
376 }
377
378 #[must_use]
380 pub fn group(mut self, group: Group<G::Identity, G::Rule, E>) -> Self {
381 self.routes.extend(group.into_routes());
382 self
383 }
384
385 #[must_use]
387 pub fn hook<F>(mut self, hook: Hook, routes: Routes, f: F) -> Self
388 where
389 F: Fn(&mut Cx<'_, G::Identity, E>) -> Result + Send + Sync + 'static,
390 {
391 self.hooks.push((hook, routes, Arc::new(f)));
392 self
393 }
394
395 #[must_use]
399 pub fn host(mut self, host: &str) -> Self {
400 self.site.hosts.push(host.to_ascii_lowercase().into());
401 self
402 }
403
404 #[must_use]
407 pub fn alias(mut self, host: &str) -> Self {
408 self.site.aliases.push(host.to_ascii_lowercase().into());
409 self
410 }
411
412 #[must_use]
417 pub fn development(mut self) -> Self {
418 self.site.development = true;
419 self
420 }
421
422 #[must_use]
426 pub fn base(mut self, base: &str) -> Self {
427 self.base = base.to_owned();
428 self
429 }
430
431 #[must_use]
433 pub fn trailing_slash(mut self, policy: TrailingSlash) -> Self {
434 self.site.slash = policy;
435 self
436 }
437
438 #[must_use]
440 pub fn lowercase_paths(mut self) -> Self {
441 self.site.lowercase = true;
442 self
443 }
444
445 #[must_use]
448 pub fn https_only(mut self) -> Self {
449 self.site.https_only = true;
450 self
451 }
452
453 #[must_use]
457 pub fn sign_in(mut self, path: &str) -> Self {
458 self.sign_in = Some(path.to_owned());
459 self
460 }
461
462 #[must_use]
465 pub fn challenge(mut self, challenge: &str) -> Self {
466 challenge.clone_into(&mut self.challenge);
467 self
468 }
469
470 #[must_use]
472 pub fn events(mut self, events: impl Events) -> Self {
473 self.events = Box::new(events);
474 self
475 }
476
477 #[must_use]
479 pub fn checkpoint(mut self, checkpoint: impl crate::Checkpoint) -> Self {
480 self.checkpoint = Some(Box::new(checkpoint));
481 self
482 }
483
484 #[must_use]
486 pub fn page_limit(mut self, bytes: usize) -> Self {
487 self.page_limit = bytes;
488 self
489 }
490
491 #[must_use]
495 pub fn wipe_buffers(mut self) -> Self {
496 self.wipe = true;
497 self
498 }
499
500 pub fn build(self) -> std::result::Result<App<G, E>, BuildError> {
508 let mut problems = self.early;
509 let mut problem = |subject: &str, message: String| {
510 problems.push(Problem {
511 subject: subject.to_owned(),
512 message,
513 });
514 };
515
516 let base = self.base.trim_end_matches('/');
518 if !base.is_empty() {
519 match url::normalize(base, self.site.lowercase) {
520 Ok(n) if n == base && !base.contains(['?', '#']) => {}
521 _ => problem(
522 "base",
523 format!("'{}' is not a canonical path such as '/blog'", self.base),
524 ),
525 }
526 }
527 for h in self.site.hosts.iter().chain(&self.site.aliases) {
528 if !valid_host(h) {
529 problem("host", format!("'{h}' is not a host name or address"));
530 }
531 }
532 if self.site.https_only && self.site.hosts.is_empty() {
533 problem(
534 "https_only",
535 "redirecting to HTTPS needs a declared host; add .host(\"example.com\")".into(),
536 );
537 }
538 if let Some(s) = &self.sign_in {
539 if !is_local_target(s) {
540 problem("sign_in", format!("'{s}' is not a site-relative path"));
541 }
542 }
543 if self.challenge.is_empty() || !self.challenge.bytes().all(|b| (0x20..0x7f).contains(&b)) {
544 problem("challenge", "the challenge must be visible ASCII".into());
545 }
546
547 if !self.security.valid() {
548 problem(
549 "security_headers",
550 "every header value must be visible ASCII and not empty".into(),
551 );
552 }
553 let mut assets = self.assets;
554 for a in &mut assets {
555 let subject = format!("assets {}", a.mount);
556 if a.mount.is_empty()
557 || !is_local_target(&a.mount)
558 || url::normalize(&a.mount, self.site.lowercase).map_or(true, |n| n != a.mount)
559 {
560 problem(
561 &subject,
562 "the mount must be a canonical path such as '/static'".into(),
563 );
564 }
565 match std::fs::canonicalize(&a.dir) {
566 Ok(root) if root.is_dir() => a.root = root,
567 _ => problem(
568 &subject,
569 format!("'{}' is not a folder that exists", a.dir.display()),
570 ),
571 }
572 for r in &self.routes {
573 if a.strip(&r.pattern).is_some() {
574 problem(
575 &r.pattern,
576 format!(
577 "the route is under the assets mount '{}' and can never run",
578 a.mount
579 ),
580 );
581 }
582 }
583 }
584 for h in &self.redirect_hosts {
585 if !valid_host(h) {
586 problem(
587 "redirect_hosts",
588 format!("'{h}' is not a host name or address"),
589 );
590 }
591 }
592 let mut health = Vec::new();
593 for (p, h) in self.health {
594 if !is_local_target(&p) || url::normalize(&p, false).map_or(true, |n| n != p) {
595 problem(
596 "health",
597 format!("'{p}' is not a canonical site-relative path"),
598 );
599 }
600 health.push((p.into_boxed_str(), h));
601 }
602
603 let mut trie = Trie::default();
605 let mut endpoints: Vec<Endpoint> = Vec::new();
606 let mut entries: Vec<Entry<G, E>> = 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, E>]> {
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 cache_values: (
703 r.cache.value(true).map(String::into_boxed_str),
704 r.cache.value(false).map(String::into_boxed_str),
705 ),
706 routed: pick(Hook::Routed),
707 finish: pick(Hook::Finish),
708 before_render: pick(Hook::BeforeRender),
709 });
710 }
711 }
712 for e in &mut endpoints {
713 let mut set = MethodSet::EMPTY;
714 for m in [
715 Method::Get,
716 Method::Head,
717 Method::Post,
718 Method::Put,
719 Method::Delete,
720 Method::Options,
721 Method::Patch,
722 ] {
723 if e.methods[m.index() as usize].is_some() {
724 set = set.with(m);
725 }
726 }
727 if set.contains(Method::Get) {
728 set = set.with(Method::Head);
729 }
730 set = set.with(Method::Options);
731 e.set = set;
732 all = all.union(set);
733 let mut v = Vec::new();
734 set.write_allow(&mut v);
735 e.allow = String::from_utf8(v).unwrap_or_default().into();
736 }
737 if !problems.is_empty() {
738 return Err(BuildError::new(problems));
739 }
740 let mut v = Vec::new();
741 all.with(Method::Options).write_allow(&mut v);
742 Ok(App {
743 gate: self.gate,
744 shared: Shared {
745 render: self.render,
746 checkpoint: self.checkpoint,
747 events: self.events,
748 page_limit: self.page_limit,
749 base: base.into(),
750 sign_in: self.sign_in.map(Into::into),
751 challenge: self.challenge.into(),
752 wipe: self.wipe,
753 security: self.security,
754 csrf: self.csrf,
755 redirect_hosts: self.redirect_hosts,
756 compression: self.compression,
757 },
758 site: self.site,
759 trie,
760 endpoints,
761 entries,
762 allow_all: String::from_utf8(v).unwrap_or_default().into(),
763 assets,
764 health,
765 })
766 }
767}
768
769impl<G: Gate> Builder<G, NoRender> {
770 #[must_use]
774 pub fn render<E: Render>(self, engine: E) -> Builder<G, E> {
775 let mut b = Builder::new(self.gate);
776 if !self.routes.is_empty() || !self.hooks.is_empty() {
777 b.early.push(Problem {
778 subject: "render".into(),
779 message:
780 "set the template engine before adding routes and hooks (or use App::with)"
781 .into(),
782 });
783 }
784 b.render = Some(engine);
785 b.site = self.site;
786 b.base = self.base;
787 b.checkpoint = self.checkpoint;
788 b.events = self.events;
789 b.page_limit = self.page_limit;
790 b.sign_in = self.sign_in;
791 b.challenge = self.challenge;
792 b.wipe = self.wipe;
793 b.security = self.security;
794 b.assets = self.assets;
795 b.health = self.health;
796 b.csrf = self.csrf;
797 b.redirect_hosts = self.redirect_hosts;
798 b.compression = self.compression;
799 b
800 }
801}
802
803impl<G: Gate, E: Render> fmt::Debug for Builder<G, E> {
804 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
805 f.debug_struct("Builder")
806 .field("routes", &self.routes)
807 .field("site", &self.site)
808 .field("base", &self.base)
809 .finish_non_exhaustive()
810 }
811}
812
813fn valid_host(h: &str) -> bool {
814 let (host, port) = split_port(h);
815 let host_ok = if let Some(inner) = host.strip_prefix('[').and_then(|r| r.strip_suffix(']')) {
816 inner.parse::<std::net::Ipv6Addr>().is_ok()
817 } else {
818 !host.is_empty()
819 && host.len() <= 253
820 && host
821 .bytes()
822 .all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'.')
823 };
824 host_ok && port.is_none_or(|p| !p.is_empty() && p.parse::<u16>().is_ok())
825}
826
827fn split_port(a: &str) -> (&str, Option<&str>) {
830 if a.starts_with('[') {
831 return match a.find(']') {
832 Some(i) => match a[i + 1..].strip_prefix(':') {
833 Some(p) => (&a[..=i], Some(p)),
834 None => (a, None),
835 },
836 None => (a, None),
837 };
838 }
839 match a.rsplit_once(':') {
840 Some((h, p)) => (h, Some(p)),
841 None => (a, None),
842 }
843}
844
845fn is_dev_host(host: &str) -> bool {
847 const SUFFIX: &[u8] = b".localhost";
848 let b = host.as_bytes();
849 ["localhost", "127.0.0.1", "[::1]"]
850 .iter()
851 .any(|d| d.eq_ignore_ascii_case(host))
852 || (b.len() > SUFFIX.len() && b[b.len() - SUFFIX.len()..].eq_ignore_ascii_case(SUFFIX))
853}
854
855fn host_parts(authority: &str) -> (&str, &str) {
857 let full = authority.trim_end_matches('.');
858 (full, split_port(full).0.trim_end_matches('.'))
859}
860
861fn host_listed(list: &[Box<str>], full: &str, host: &str) -> bool {
864 list.iter().any(|h| {
865 h.eq_ignore_ascii_case(full) || (split_port(h).1.is_none() && h.eq_ignore_ascii_case(host))
866 })
867}
868
869thread_local! {
870 static SCRATCH: Cell<Option<Box<Out>>> = const { Cell::new(None) };
874}
875
876const KEEP_CAPACITY: usize = 1 << 20;
878
879enum Placed<'a> {
881 Here(Cow<'a, str>),
882 Redirect(String),
883 Refuse(u16, &'static str, Option<url::UrlError>),
884}
885
886impl<G: Gate, E: Render> App<G, E> {
887 pub fn respond(&self, req: &HttpRequest<'_>, env: &Envelope, reply: &mut HttpReply) -> Outcome {
889 self.serve(req, env, None, reply).outcome
890 }
891
892 pub fn serve(
897 &self,
898 req: &HttpRequest<'_>,
899 env: &Envelope,
900 who: Option<&G::Identity>,
901 reply: &mut HttpReply,
902 ) -> Served {
903 let mut report = Served {
904 outcome: Outcome::Done,
905 route: None,
906 cache: Cacheable::No,
907 };
908 report.outcome = self.run(req, env, who, reply, &mut report);
909 report
910 }
911
912 pub fn respond_identified(
917 &self,
918 req: &HttpRequest<'_>,
919 env: &Envelope,
920 who: &G::Identity,
921 reply: &mut HttpReply,
922 ) -> Outcome {
923 self.serve(req, env, Some(who), reply).outcome
924 }
925
926 #[must_use]
929 pub fn classify(&self, head: &HttpHead<'_>) -> Classified {
930 let none = Classified {
931 route: None,
932 work: WorkClass::Quick,
933 };
934 let Placed::Here(path) = self.place(head.authority(), head.path(), Scheme::Https, true)
935 else {
936 return none;
937 };
938 let Some((ep, _)) = self.trie.find(&path) else {
939 return none;
940 };
941 match self.entry_for(ep as usize, head.method()) {
942 Some(i) => Classified {
943 route: Some(RouteId(i)),
944 work: self.entries[i as usize].work,
945 },
946 None => none,
947 }
948 }
949
950 #[must_use]
952 pub fn gate(&self) -> &G {
953 &self.gate
954 }
955
956 fn entry_for(&self, ep: usize, m: Method) -> Option<u32> {
957 let methods = &self.endpoints[ep].methods;
958 match m {
959 Method::Other => None,
960 Method::Head => {
961 methods[Method::Head.index() as usize].or(methods[Method::Get.index() as usize])
962 }
963 m => methods[m.index() as usize],
964 }
965 }
966
967 fn place<'a>(
970 &self,
971 authority: Option<&'a str>,
972 raw: &'a str,
973 scheme: Scheme,
974 quiet: bool,
975 ) -> Placed<'a> {
976 let site = &self.site;
977 let authority = authority.unwrap_or("");
978 let (full, host_only) = host_parts(authority);
979 let listed = |list: &[Box<str>]| host_listed(list, full, host_only);
980 let dev = site.development && is_dev_host(host_only);
981 let mut target_host: Option<&str> = None;
982 if !site.hosts.is_empty() && !dev && !listed(&site.hosts) {
985 if listed(&site.aliases) {
986 target_host = site.hosts.first().map(AsRef::as_ref);
987 } else {
988 return Placed::Refuse(421, "host.unknown", None);
989 }
990 }
991 let mut https = scheme == Scheme::Https;
992 if site.https_only && !https && !dev {
993 https = true;
994 target_host = target_host.or(site.hosts.first().map(AsRef::as_ref));
995 }
996
997 let canon = match url::normalize(raw, site.lowercase) {
999 Ok(c) => c,
1000 Err(e) => return Placed::Refuse(400, e.code(), Some(e)),
1001 };
1002 let base = self.shared.base.as_ref();
1003 let c: &str = &canon;
1004 let rel_start = if base.is_empty() {
1005 0
1006 } else if c == base {
1007 c.len()
1008 } else if c.strip_prefix(base).is_some_and(|r| r.starts_with('/')) {
1009 base.len()
1010 } else {
1011 return Placed::Refuse(404, "site.outside_base", None);
1012 };
1013 let rel = &c[rel_start..];
1014 let mut fixed: Option<String> = None;
1015 let health = self.health.iter().any(|(p, _)| p.as_ref() == rel);
1016 if rel.is_empty() {
1017 fixed = Some("/".into());
1018 } else if rel.len() > 1 && !health {
1019 match site.slash {
1020 TrailingSlash::Strip => {
1021 if let Some(s) = rel.strip_suffix('/') {
1022 fixed = Some(s.to_owned());
1023 }
1024 }
1025 TrailingSlash::Add => {
1026 let trimmed = rel.strip_suffix('/').unwrap_or(rel);
1027 let file_like = trimmed.rsplit('/').next().unwrap_or("").contains('.');
1028 if file_like && rel.ends_with('/') {
1029 fixed = Some(trimmed.to_owned());
1031 } else if !file_like && !rel.ends_with('/') {
1032 fixed = Some(format!("{rel}/"));
1033 }
1034 }
1035 TrailingSlash::Ignore => {}
1036 }
1037 }
1038 if target_host.is_none() && fixed.is_none() {
1039 if let Cow::Borrowed(c) = canon {
1040 let mut path = &c[rel_start..];
1041 if site.slash == TrailingSlash::Ignore && path.len() > 1 {
1042 path = path.strip_suffix('/').unwrap_or(path);
1043 }
1044 return Placed::Here(Cow::Borrowed(path));
1045 }
1046 }
1047 if quiet {
1048 return Placed::Refuse(0, "", None);
1049 }
1050 let rel_out = fixed.as_deref().unwrap_or(rel);
1051 let mut loc = String::with_capacity(raw.len() + 32);
1052 if let Some(h) = target_host {
1053 loc.push_str(if https { "https://" } else { "http://" });
1054 loc.push_str(h);
1055 }
1056 loc.push_str(base);
1057 loc.push_str(rel_out);
1058 Placed::Redirect(loc)
1059 }
1060
1061 fn run(
1062 &self,
1063 http: &HttpRequest<'_>,
1064 env: &Envelope,
1065 who: Option<&G::Identity>,
1066 reply: &mut HttpReply,
1067 report: &mut Served,
1068 ) -> Outcome {
1069 let method = http.method();
1070 let raw_path = http.path();
1071 let authority = http.head().authority();
1072 let at = At {
1073 env,
1074 tls: self.tls(env, authority),
1075 json: wants_json(http.header("accept")),
1076 };
1077 if raw_path == "*" {
1078 if self.claim(authority, &self.shared.base).is_none() {
1081 self.refuse(
1082 421,
1083 "host.unknown",
1084 EventKind::Refused,
1085 None,
1086 None,
1087 at,
1088 reply,
1089 );
1090 return Outcome::Done;
1091 }
1092 if method == Method::Options {
1093 reply.status(204).header("allow", self.allow_all.as_bytes());
1094 self.secure(reply, at.tls, |_| false);
1095 } else {
1096 self.refuse(
1097 400,
1098 "url.asterisk",
1099 EventKind::Refused,
1100 None,
1101 None,
1102 at,
1103 reply,
1104 );
1105 }
1106 return Outcome::Done;
1107 }
1108 let host_raw = authority.unwrap_or("");
1109 let placed = self.place(authority, raw_path, env.client().scheme(), false);
1110 let path = match placed {
1111 Placed::Here(path) => path,
1112 Placed::Redirect(mut loc) => {
1113 if let Some(q) = http.query() {
1114 loc.push('?');
1115 loc.push_str(q);
1116 }
1117 let status = if matches!(method, Method::Get | Method::Head) {
1118 301
1119 } else {
1120 308
1121 };
1122 reply.status(status).header("location", loc.as_bytes());
1123 self.secure(reply, at.tls, |_| false);
1124 return Outcome::Done;
1125 }
1126 Placed::Refuse(status, code, src) => {
1127 let src = src
1128 .as_ref()
1129 .map(|e| e as &(dyn std::error::Error + 'static));
1130 self.refuse(status, code, EventKind::Refused, None, src, at, reply);
1131 return Outcome::Done;
1132 }
1133 };
1134 if let Some(outcome) = self.early(http, at, &path, reply) {
1136 return outcome;
1137 }
1138 if let Some(outcome) = self.preflight(http, at, &path, reply) {
1140 return outcome;
1141 }
1142
1143 let req = Request {
1144 http,
1145 env,
1146 path: &path,
1147 host: host_raw,
1148 route: None,
1149 tls: at.tls,
1150 json: at.json,
1151 };
1152
1153 let owned;
1155 let owned_here = who.is_none();
1156 let who: &G::Identity = match who {
1157 Some(w) => w,
1158 None => match self.gate.authenticate(&req) {
1159 Ok(w) => {
1160 owned = w;
1161 &owned
1162 }
1163 Err(d) => {
1164 self.deny(d, &req, reply);
1165 return Outcome::Done;
1166 }
1167 },
1168 };
1169
1170 let mut gate_cookie = if owned_here { who.set_cookie() } else { None };
1171 let outcome = self.routed(http, env, at, req, who, &mut gate_cookie, reply, report);
1172 if let Some(c) = gate_cookie {
1178 reply.header("set-cookie", c.as_bytes());
1179 report.cache = Cacheable::No;
1180 }
1181 outcome
1182 }
1183
1184 #[allow(clippy::too_many_arguments)]
1186 fn routed<'r>(
1187 &'r self,
1188 http: &HttpRequest<'_>,
1189 env: &Envelope,
1190 at: At<'_>,
1191 mut req: Request<'r>,
1192 who: &G::Identity,
1193 gate_cookie: &mut Option<&str>,
1194 reply: &mut HttpReply,
1195 report: &mut Served,
1196 ) -> Outcome {
1197 let method = http.method();
1198 let path = req.path;
1199 let host_raw = req.host;
1200 let Some((ep, caps)) = self.trie.find(path) else {
1202 self.refuse(404, "route.none", EventKind::Refused, None, None, at, reply);
1203 return Outcome::Done;
1204 };
1205 let endpoint = &self.endpoints[ep as usize];
1206 let Some(idx) = self.entry_for(ep as usize, method) else {
1207 if method == Method::Options {
1208 reply.status(204).header("allow", endpoint.allow.as_bytes());
1209 self.secure(reply, at.tls, |_| false);
1210 return Outcome::Done;
1211 }
1212 let status = if method == Method::Other { 501 } else { 405 };
1213 let code = if status == 501 {
1214 "method.unknown"
1215 } else {
1216 "method.not_allowed"
1217 };
1218 self.refuse(status, code, EventKind::Refused, None, None, at, reply);
1219 if status == 405 {
1220 reply.header("allow", endpoint.allow.as_bytes());
1221 }
1222 return Outcome::Done;
1223 };
1224 let entry = &self.entries[idx as usize];
1225 req.route = Some(&entry.pattern);
1226 report.route = Some(RouteId(idx));
1227
1228 if let Access::Rule(rule) = &entry.access {
1230 if let Err(d) = self.gate.authorize(who, rule, &req) {
1231 self.deny(d, &req, reply);
1232 return Outcome::Done;
1233 }
1234 }
1235 if let Some(cp) = &self.shared.checkpoint {
1236 match cp.check(env, Stage::Authorized) {
1237 Verdict::Pass => {}
1238 Verdict::Deny { status } => {
1239 self.refuse(
1240 status,
1241 "checkpoint.denied",
1242 EventKind::Revoked,
1243 req.route,
1244 None,
1245 at,
1246 reply,
1247 );
1248 return Outcome::Done;
1249 }
1250 _ => {
1251 self.refuse(
1252 403,
1253 "checkpoint.killed",
1254 EventKind::Revoked,
1255 req.route,
1256 None,
1257 at,
1258 reply,
1259 );
1260 return Outcome::Close;
1261 }
1262 }
1263 }
1264
1265 let binding = who.csrf_binding();
1267 match (&self.shared.csrf, entry.csrf) {
1268 (Some(engine), true) => {
1269 let scheme = if env.client().scheme() == Scheme::Https {
1270 "https://"
1271 } else {
1272 "http://"
1273 };
1274 let own = format!("{scheme}{}", host_raw.to_ascii_lowercase());
1275 match csrf::check(engine, http, binding, &own, &entry.limits) {
1276 Ok(()) => {}
1277 Err(code) => {
1278 self.refuse(403, code, EventKind::Denied, req.route, None, at, reply);
1279 return Outcome::Done;
1280 }
1281 }
1282 }
1283 (None, true) => {
1285 self.refuse(
1286 500,
1287 "csrf.no_engine",
1288 EventKind::Failed,
1289 req.route,
1290 None,
1291 at,
1292 reply,
1293 );
1294 return Outcome::Done;
1295 }
1296 (_, false) => {}
1297 }
1298
1299 let mut out = SCRATCH.with(Cell::take).unwrap_or_default();
1301 out.reset();
1302 let outcome = self.handle(
1303 entry,
1304 req,
1305 who,
1306 caps,
1307 (gate_cookie, binding),
1308 &mut out,
1309 reply,
1310 &mut report.cache,
1311 );
1312 if self.shared.wipe {
1313 out.wipe();
1314 }
1315 if out.body.capacity() <= KEEP_CAPACITY && out.values.capacity() <= KEEP_CAPACITY {
1316 SCRATCH.with(|s| s.set(Some(out)));
1317 }
1318 outcome
1319 }
1320
1321 fn preflight(
1323 &self,
1324 http: &HttpRequest<'_>,
1325 at: At<'_>,
1326 path: &str,
1327 reply: &mut HttpReply,
1328 ) -> Option<Outcome> {
1329 if http.method() != Method::Options {
1330 return None;
1331 }
1332 let origin = http.header("origin")?;
1333 let wanted = Method::from_bytes(http.header("access-control-request-method")?);
1334 let (ep, _) = self.trie.find(path)?;
1335 let ep = ep as usize;
1336 if self.entry_for(ep, Method::Options).is_some() {
1337 return None;
1338 }
1339 let cors = self
1340 .entry_for(ep, wanted)
1341 .and_then(|i| self.entries[i as usize].cors.as_ref())?;
1342 let mut set = MethodSet::EMPTY;
1344 for m in self.endpoints[ep].set.iter() {
1345 let shares = self.entry_for(ep, m).is_some_and(|i| {
1346 self.entries[i as usize]
1347 .cors
1348 .as_ref()
1349 .is_some_and(|c| Arc::ptr_eq(c, cors))
1350 });
1351 if shares {
1352 set = set.with(m);
1353 }
1354 }
1355 let ok = cors.preflight(
1356 origin,
1357 wanted,
1358 http.header("access-control-request-headers"),
1359 set,
1360 reply,
1361 );
1362 if !ok {
1363 self.event(EventKind::Refused, at.env, None, 204, "cors.denied", None);
1364 }
1365 self.secure(reply, at.tls, |_| false);
1366 Some(Outcome::Done)
1367 }
1368
1369 fn early(
1371 &self,
1372 http: &HttpRequest<'_>,
1373 at: At<'_>,
1374 path: &str,
1375 reply: &mut HttpReply,
1376 ) -> Option<Outcome> {
1377 let method = http.method();
1378 let get_like = matches!(method, Method::Get | Method::Head);
1379 if let Some((_, h)) = self.health.iter().find(|(p, _)| p.as_ref() == path) {
1380 if !get_like {
1381 self.not_get(method, at, reply);
1382 return Some(Outcome::Done);
1383 }
1384 let (status, text) = match h {
1385 Health::Live => (200, "ok"),
1386 Health::Ready(f) if f() => (200, "ready"),
1387 Health::Ready(_) => (503, "not ready"),
1388 };
1389 reply.text(status, text).header("cache-control", "no-store");
1390 self.secure(reply, at.tls, |_| false);
1391 return Some(Outcome::Done);
1392 }
1393 let (assets, rel) = self
1394 .assets
1395 .iter()
1396 .find_map(|a| a.strip(path).map(|rel| (a, rel)))?;
1397 if !get_like {
1398 self.not_get(method, at, reply);
1399 return Some(Outcome::Done);
1400 }
1401 let found = assets::find(assets, rel, http.header("accept-encoding"));
1402 let key = (at.env.connection().get(), at.env.seq().get());
1403 if let Some(tag) = assets::continuing(key) {
1404 return Some(assets::next_part(key, &tag, found.as_ref(), http, reply));
1407 }
1408 let Some(found) = found else {
1409 self.refuse(
1410 404,
1411 "assets.none",
1412 EventKind::Refused,
1413 None,
1414 None,
1415 at,
1416 reply,
1417 );
1418 return Some(Outcome::Done);
1419 };
1420 let Some(outcome) = assets::serve(assets, &found, key, http, reply) else {
1421 self.refuse(
1422 500,
1423 "assets.unreadable",
1424 EventKind::Failed,
1425 None,
1426 None,
1427 at,
1428 reply,
1429 );
1430 return Some(Outcome::Done);
1431 };
1432 self.secure(reply, at.tls, |_| false);
1433 Some(outcome)
1434 }
1435
1436 fn not_get(&self, method: Method, at: At<'_>, reply: &mut HttpReply) {
1438 if method == Method::Options {
1439 reply.status(204).header("allow", "GET, HEAD, OPTIONS");
1440 self.secure(reply, at.tls, |_| false);
1441 } else {
1442 let (status, code) = if method == Method::Other {
1443 (501, "method.unknown")
1444 } else {
1445 (405, "method.not_allowed")
1446 };
1447 self.refuse(status, code, EventKind::Refused, None, None, at, reply);
1448 if status == 405 {
1449 reply.header("allow", "GET, HEAD, OPTIONS");
1450 }
1451 }
1452 }
1453
1454 #[allow(clippy::too_many_arguments)]
1455 fn handle(
1456 &self,
1457 entry: &Entry<G, E>,
1458 req: Request<'_>,
1459 who: &G::Identity,
1460 caps: Captures,
1461 (gate_cookie, binding): (&mut Option<&str>, Option<&[u8]>),
1462 out: &mut Out,
1463 reply: &mut HttpReply,
1464 cacheable: &mut Cacheable,
1465 ) -> Outcome {
1466 let at = At {
1467 env: req.env,
1468 tls: req.tls,
1469 json: req.json,
1470 };
1471 let route = req.route;
1472 let http = req.http;
1473 let guest = who.is_guest();
1474 let result = {
1475 let mut cx = Cx {
1476 req,
1477 who,
1478 caps,
1479 names: &entry.names,
1480 out: &mut *out,
1481 shared: &self.shared,
1482 template: entry.template,
1483 binding,
1484 limits: &entry.limits,
1485 before_render: &entry.before_render,
1486 rendering: false,
1487 };
1488 catch_unwind(AssertUnwindSafe(|| -> Result {
1489 for h in entry.routed.iter() {
1490 h(&mut cx)?;
1491 }
1492 entry.handler.call(&mut cx)?;
1493 for h in entry.finish.iter() {
1494 h(&mut cx)?;
1495 }
1496 Ok(())
1497 }))
1498 };
1499 match result {
1500 Ok(Ok(())) if !out.invalid => {
1501 if gate_cookie.is_some_and(|c| out.sets_cookie(c)) {
1504 *gate_cookie = None;
1505 }
1506 let cookies = gate_cookie.is_some() || out.has("set-cookie");
1511 let ours = !out.has("cache-control");
1512 if cookies {
1513 let public = out
1514 .get("cache-control")
1515 .is_some_and(|v| v.windows(6).any(|w| w.eq_ignore_ascii_case(b"public")));
1516 if ours || public {
1517 out.remove("cache-control");
1518 out.header("cache-control", b"private, no-store");
1519 }
1520 } else if ours {
1521 let (for_guest, for_member) = &entry.cache_values;
1522 let value = if guest { for_guest } else { for_member };
1523 if let Some(v) = value {
1524 out.header("cache-control", v.as_bytes());
1525 }
1526 }
1527 if let Some(c) = self.shared.compression {
1528 compress(c, http.header("accept-encoding"), out);
1529 }
1530 *cacheable = match entry.cache {
1531 CachePolicy::Guests { max_age } | CachePolicy::Public { max_age }
1532 if guest
1533 && ours
1534 && !cookies
1535 && entry.cors.is_none()
1536 && matches!(http.method(), Method::Get | Method::Head)
1537 && out.status == 200
1538 && out
1539 .get("vary")
1540 .is_none_or(|v| v.eq_ignore_ascii_case(b"accept-encoding")) =>
1541 {
1542 Cacheable::Guests { max_age }
1543 }
1544 _ => Cacheable::No,
1545 };
1546 reply.status(out.status);
1547 if let Some(c) = gate_cookie.take() {
1548 reply.header("set-cookie", c.as_bytes());
1549 }
1550 for &(ns, ne, vs, ve) in &out.spans {
1551 let name = core::str::from_utf8(&out.names[ns as usize..ne as usize])
1552 .unwrap_or("x-invalid");
1553 reply.header(name, &out.values[vs as usize..ve as usize]);
1554 }
1555 if !out.has("x-content-type-options") {
1556 reply.header("x-content-type-options", "nosniff");
1557 }
1558 if let Some(cors) = &entry.cors {
1559 cors.actual(http.header("origin"), reply);
1560 }
1561 self.secure(reply, at.tls, |n| out.has(n));
1562 reply.body(&out.body);
1563 Outcome::Done
1564 }
1565 Ok(Ok(())) => {
1566 out.reset();
1567 self.refuse(
1568 500,
1569 "reply.invalid_field",
1570 EventKind::Failed,
1571 route,
1572 None,
1573 at,
1574 reply,
1575 );
1576 Outcome::Done
1577 }
1578 Ok(Err(e)) => {
1579 out.reset();
1580 let src = std::error::Error::source(&e);
1581 self.refuse(
1582 e.status(),
1583 e.code(),
1584 EventKind::Failed,
1585 route,
1586 src,
1587 at,
1588 reply,
1589 );
1590 Outcome::Done
1591 }
1592 Err(_) => {
1593 out.reset();
1594 self.refuse(
1595 500,
1596 "handler.panic",
1597 EventKind::Panicked,
1598 route,
1599 None,
1600 at,
1601 reply,
1602 );
1603 Outcome::Done
1604 }
1605 }
1606 }
1607
1608 fn deny(&self, d: Denial, req: &Request<'_>, reply: &mut HttpReply) {
1610 let env = req.env;
1611 let at = At {
1612 env,
1613 tls: req.tls,
1614 json: req.json,
1615 };
1616 if d.kind() == DenialKind::SignIn {
1617 if let Some(page) = &self.shared.sign_in {
1618 if matches!(req.method(), Method::Get | Method::Head) {
1619 let mut loc = String::with_capacity(64);
1620 loc.push_str(&self.shared.base);
1621 loc.push_str(page);
1622 loc.push_str(if page.contains('?') {
1623 "&next="
1624 } else {
1625 "?next="
1626 });
1627 let mut next = String::new();
1628 next.push_str(&self.shared.base);
1629 next.push_str(req.path);
1630 if let Some(q) = req.query() {
1631 next.push('?');
1632 next.push_str(q);
1633 }
1634 encode_component(&next, &mut loc);
1635 self.event(EventKind::Denied, env, req.route, 303, d.code(), None);
1636 reply.status(303).header("location", loc.as_bytes());
1637 self.secure(reply, at.tls, |_| false);
1638 if let Some(c) = d.cookie() {
1639 reply.header("set-cookie", c.as_bytes());
1640 }
1641 return;
1642 }
1643 }
1644 }
1645 let status = d.status_code();
1646 self.refuse(
1647 status,
1648 d.code(),
1649 EventKind::Denied,
1650 req.route,
1651 None,
1652 at,
1653 reply,
1654 );
1655 if status == 401 {
1656 reply.header("www-authenticate", self.shared.challenge.as_bytes());
1657 }
1658 if let Some(c) = d.cookie() {
1659 reply.header("set-cookie", c.as_bytes());
1660 }
1661 }
1662
1663 fn event(
1664 &self,
1665 kind: EventKind,
1666 env: &Envelope,
1667 route: Option<&str>,
1668 status: u16,
1669 code: &'static str,
1670 source: Option<&(dyn std::error::Error + 'static)>,
1671 ) {
1672 self.shared.events.event(&Event {
1673 kind,
1674 id: RequestId::of(env),
1675 route,
1676 status,
1677 code,
1678 source,
1679 });
1680 }
1681
1682 #[allow(clippy::too_many_arguments)]
1686 fn refuse(
1687 &self,
1688 status: u16,
1689 code: &'static str,
1690 kind: EventKind,
1691 route: Option<&str>,
1692 source: Option<&(dyn std::error::Error + 'static)>,
1693 at: At<'_>,
1694 reply: &mut HttpReply,
1695 ) {
1696 self.event(kind, at.env, route, status, code, source);
1697 let reference = RequestId::of(at.env).to_string();
1698 let mut body = Vec::with_capacity(512);
1699 if at.json {
1700 error::json_body(status, &reference, &mut body);
1701 reply
1702 .status(status)
1703 .header("content-type", "application/json")
1704 .header("x-content-type-options", "nosniff")
1705 .header("cache-control", "no-store")
1706 .body(&body);
1707 self.secure(reply, at.tls, |_| false);
1708 return;
1709 }
1710 let mut done = false;
1711 if let Some(engine) = self.shared.render.as_ref() {
1712 let page = ErrorPage {
1713 status,
1714 title: error::title(status),
1715 message: error::message(status),
1716 reference: &reference,
1717 };
1718 let mut out = Output::new(&mut body, self.shared.page_limit);
1719 let wrote = engine.error_page(&page, &mut out);
1720 if matches!(wrote, Ok(true)) && !out.over() {
1721 done = true;
1722 } else {
1723 body.clear();
1724 }
1725 }
1726 if !done {
1727 error::builtin_page(status, &reference, &mut body);
1728 }
1729 reply
1730 .status(status)
1731 .header("content-type", "text/html; charset=utf-8")
1732 .header("x-content-type-options", "nosniff")
1733 .header("cache-control", "no-store")
1734 .body(&body);
1735 self.secure(reply, at.tls, |_| false);
1736 }
1737
1738 fn secure(&self, reply: &mut HttpReply, tls: bool, has: impl Fn(&str) -> bool) {
1741 for (name, value) in self.shared.security.fields(tls) {
1742 if !has(name) {
1743 reply.header(name, value.as_bytes());
1744 }
1745 }
1746 }
1747
1748 pub(crate) fn claim(&self, authority: Option<&str>, path: &str) -> Option<(u8, usize)> {
1752 let (full, host_only) = host_parts(authority.unwrap_or(""));
1753 let listed = |list: &[Box<str>]| host_listed(list, full, host_only);
1754 let rank = if self.site.hosts.is_empty() {
1755 0
1756 } else if listed(&self.site.hosts) || listed(&self.site.aliases) {
1757 2
1758 } else if self.site.development && is_dev_host(host_only) {
1759 1
1760 } else {
1761 return None;
1762 };
1763 let base = self.shared.base.as_ref();
1764 let head = path.get(..base.len());
1767 let starts = head.is_some_and(|h| {
1768 if self.site.lowercase {
1769 h.eq_ignore_ascii_case(base)
1770 } else {
1771 h == base
1772 }
1773 });
1774 let inside = base.is_empty()
1775 || (starts && matches!(path.as_bytes().get(base.len()), None | Some(b'/')));
1776 inside.then_some((rank, base.len()))
1777 }
1778
1779 fn tls(&self, env: &Envelope, authority: Option<&str>) -> bool {
1782 if env.client().scheme() != Scheme::Https {
1783 return false;
1784 }
1785 let (_, host) = host_parts(authority.unwrap_or(""));
1786 !(self.site.development && is_dev_host(host))
1787 }
1788}
1789
1790impl<G: Gate, E: Render> HttpService for App<G, E> {
1791 fn call(&self, request: &HttpRequest<'_>, env: &Envelope, reply: &mut HttpReply) -> Outcome {
1792 self.respond(request, env, reply)
1793 }
1794}
1795
1796impl<G: Gate, E: Render> fmt::Debug for App<G, E> {
1797 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1798 f.debug_struct("App")
1799 .field("site", &self.site)
1800 .field("base", &self.shared.base)
1801 .field("routes", &self.entries.len())
1802 .finish_non_exhaustive()
1803 }
1804}
1805
1806fn wants_json(accept: Option<&[u8]>) -> bool {
1808 accept.is_some_and(|a| a.windows(4).any(|w| w.eq_ignore_ascii_case(b"json")))
1810 && parse_net::best_media(accept, &["text/html", "application/json"]) == Some(1)
1811}
1812
1813fn compress(c: Compression, accept_encoding: Option<&[u8]>, out: &mut Out) {
1817 if out.status != 200 || out.has("content-encoding") {
1818 return;
1819 }
1820 if !out.get("content-type").is_some_and(compressible) {
1821 return;
1822 }
1823 if !out.has("vary") {
1824 out.header("vary", b"accept-encoding");
1825 }
1826 let accepts = accept_encoding
1827 .and_then(|v| parse_net::best_encoding(Some(v), &["gzip", "identity"]))
1828 == Some(0);
1829 if !accepts || out.body.len() < c.min_bytes {
1830 return;
1831 }
1832 let level = compress_lib::Level::new(c.level).unwrap_or(compress_lib::Level::DEFAULT);
1833 let packed = compress_lib::gzip::compress_with(&out.body, level);
1834 if packed.len() < out.body.len() {
1835 out.body.clear();
1836 out.body.extend_from_slice(&packed);
1837 out.header("content-encoding", b"gzip");
1838 }
1839}
1840
1841fn encode_component(s: &str, out: &mut String) {
1843 const HEX: &[u8; 16] = b"0123456789ABCDEF";
1844 for b in s.bytes() {
1845 if b.is_ascii_alphanumeric() || matches!(b, b'-' | b'.' | b'_' | b'~' | b'/') {
1846 out.push(b as char);
1847 } else {
1848 out.push('%');
1849 out.push(HEX[(b >> 4) as usize] as char);
1850 out.push(HEX[(b & 15) as usize] as char);
1851 }
1852 }
1853}
1854
1855#[cfg(test)]
1856mod tests {
1857 use super::*;
1858
1859 #[test]
1860 fn test_split_port() {
1861 assert_eq!(split_port("a.test:80"), ("a.test", Some("80")));
1862 assert_eq!(split_port("a.test"), ("a.test", None));
1863 assert_eq!(split_port("[::1]:8080"), ("[::1]", Some("8080")));
1864 assert_eq!(split_port("[::1]"), ("[::1]", None));
1865 }
1866
1867 #[test]
1868 fn test_valid_host() {
1869 for ok in ["example.com", "localhost:8080", "[::1]", "127.0.0.1:80"] {
1870 assert!(valid_host(ok), "{ok}");
1871 }
1872 for bad in ["", "a b", "a:", "a:x", "[zz]", "a/b"] {
1873 assert!(!valid_host(bad), "{bad}");
1874 }
1875 }
1876
1877 #[test]
1878 fn test_selector() {
1879 assert!(Routes::prefix("/blog").matches("/blog", None));
1880 assert!(Routes::prefix("/blog/").matches("/blog/:slug", None));
1881 assert!(!Routes::prefix("/blog").matches("/blogger", None));
1882 assert!(Routes::named(&["a"]).matches("/x", Some("a")));
1883 assert!(!Routes::named(&["a"]).matches("/x", None));
1884 }
1885}