Skip to main content

web_net/
app.rs

1//! The application: its site, its compiled routes, its gate, and the
2//! pipeline every request runs.
3
4use 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
31/// The default budget for one rendered page.
32pub const DEFAULT_PAGE_LIMIT: usize = 8 << 20;
33
34/// A named point in the pipeline where registered code runs, only for the
35/// routes it registered for. A route with no hooks pays nothing.
36#[non_exhaustive]
37#[derive(Clone, Copy, Debug, PartialEq, Eq)]
38pub enum Hook {
39    /// After the access check passed, before the handler: locale, feature
40    /// flags, an A/B variant. An error ends the request.
41    Routed,
42    /// After the handler succeeded, before the reply leaves: add headers.
43    /// An error replaces the reply with the error page.
44    Finish,
45    /// Inside [`Cx::render`], before the engine runs: choose a variant, add
46    /// headers. A hook that renders does not run these hooks again.
47    BeforeRender,
48}
49
50/// Which routes a hook runs for.
51#[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    /// Every route.
63    #[must_use]
64    pub fn all() -> Self {
65        Self(Selector::All)
66    }
67
68    /// Routes whose pattern starts with `prefix` (`/blog`).
69    #[must_use]
70    pub fn prefix(prefix: impl Into<String>) -> Self {
71        Self(Selector::Prefix(prefix.into()))
72    }
73
74    /// Routes with one of these names.
75    #[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/// What [`App::classify`] tells a dispatcher before admission.
98#[non_exhaustive]
99#[derive(Clone, Copy, Debug, PartialEq, Eq)]
100pub struct Classified {
101    /// The route that will run, if one matched.
102    pub route: Option<RouteId>,
103    /// How its handler behaves (`Quick` when nothing matched).
104    pub work: WorkClass,
105}
106
107/// A declared route (one method of one pattern), numbered in declaration
108/// order.
109#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
110pub struct RouteId(pub u32);
111
112/// The parts of the application every handler can reach.
113pub(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/// Where a reply is going: the request's envelope, and whether security
129/// headers for HTTPS apply (HTTPS, and not a development host).
130#[derive(Clone, Copy)]
131pub(crate) struct At<'a> {
132    pub(crate) env: &'a Envelope,
133    pub(crate) tls: bool,
134    /// The client prefers JSON to HTML: error replies are JSON.
135    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    /// Lower case; the first is canonical.
149    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-Control` for guests and for signed-in visitors, made once.
169    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    /// Entry per method, by `Method::index`.
177    methods: [Option<u32>; 10],
178    allow: Box<str>,
179    set: MethodSet,
180}
181
182/// A web application: one site, its routes, and its gate. Built once at
183/// start-up with [`App::builder`] or [`App::with_gate`]; immutable after.
184///
185/// It is a `serve_net::HttpService`, so it runs on serve-net as it is, and
186/// it has [`respond`](Self::respond) and
187/// [`respond_identified`](Self::respond_identified) for a wrapper that
188/// calls it from its own service.
189///
190/// ```
191/// use web_net::{App, get};
192///
193/// let app = App::builder()
194///     .route(get("/", |cx| { cx.html(200, "<h1>Hello</h1>"); Ok(()) }).public())
195///     .route(get("/users/:id", |cx| {
196///         let id: u64 = cx.param_as("id")?;
197///         cx.text(200, &format!("user {id}"));
198///         Ok(())
199///     }).public())
200///     .build()?;
201///
202/// let reply = serve_net::testing::respond(&app, b"GET /users/7 HTTP/1.1\r\nHost: a\r\n\r\n");
203/// assert_eq!((reply.status, reply.text()), (200, "user 7"));
204///
205/// let reply = serve_net::testing::respond(&app, b"GET /users/7/ HTTP/1.1\r\nHost: a\r\n\r\n");
206/// assert_eq!(reply.status, 301);
207/// assert_eq!(reply.header("location"), Some(&b"/users/7"[..]));
208/// # Ok::<(), web_net::BuildError>(())
209/// ```
210pub 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    /// A builder with the [`Open`] gate: every visitor is a guest and every
224    /// route is declared public.
225    #[must_use]
226    pub fn builder() -> Builder<Open> {
227        Builder::new(Open)
228    }
229}
230
231impl<G: Gate> App<G> {
232    /// A builder with `gate` for authentication and authorization.
233    #[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    /// A builder with `gate` and a template engine. Handlers render with
241    /// [`Cx::render`](crate::Cx::render) and the engine's view type (the
242    /// application's own type for a typed engine), or with
243    /// [`Cx::render_view`](crate::Cx::render_view) for an engine that reads
244    /// any [`View`](crate::View).
245    #[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
253/// Declares an application. Every mistake is reported by
254/// [`build`](Self::build) at once, before anything listens.
255pub 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    /// Problems found before `build` (an engine set after routes).
275    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    /// Compress replies with gzip when the client accepts it: HTML, CSS,
304    /// JavaScript, JSON, XML, SVG, and other text, over a minimum size. Off
305    /// by default: a proxy in front usually compresses.
306    #[must_use]
307    pub fn compress(mut self, compression: Compression) -> Self {
308        self.compression = Some(compression);
309        self
310    }
311
312    /// CSRF protection with csrf-sec (re-exported as
313    /// [`web_net::csrf_sec`](crate::csrf_sec)), built from a persisted
314    /// 32-byte key. Required as soon as one route takes an unsafe method and
315    /// is not [exempt](crate::Route::csrf_exempt).
316    ///
317    /// web-net always applies the origin policy and a token, whatever the
318    /// `Csrf`'s `Mode`; its keys, lifetimes, and allowed origins apply as set.
319    #[must_use]
320    pub fn csrf(mut self, csrf: csrf_sec::Csrf) -> Self {
321        self.csrf = Some(csrf);
322        self
323    }
324
325    /// Hosts that [`Cx::redirect_external`] may send a visitor to
326    /// (`accounts.example.com`). Everything else is refused.
327    #[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    /// Serves a folder of files under a mount path, before routing and
338    /// without the gate (files are public).
339    #[must_use]
340    pub fn assets(mut self, assets: Assets) -> Self {
341        self.assets.push(assets);
342        self
343    }
344
345    /// A liveness path (`/healthz`): `200 ok` whenever the process answers.
346    /// Answered before routing and without the gate.
347    #[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    /// A readiness path (`/readyz`): `200 ready` when `check` says so,
354    /// `503 not ready` otherwise. Keep `check` to a flag read: it runs on the
355    /// request path.
356    #[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    /// The security headers every reply carries (default
364    /// [`SecurityHeaders::default`]).
365    #[must_use]
366    pub fn security_headers(mut self, policy: SecurityHeaders) -> Self {
367        self.security = policy;
368        self
369    }
370
371    /// Adds a route.
372    #[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    /// Adds a group's routes.
379    #[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    /// Runs `f` at `hook` for the routes `routes` selects.
386    #[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    /// A host the site answers (`example.com`, or `localhost:8080` with its
396    /// port). The first one is canonical. With no host declared the site
397    /// answers any host; with hosts declared, any other host gets 421.
398    #[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    /// A host that is redirected (301, or 308 for unsafe methods) to the
405    /// canonical host (`www.example.com` to `example.com`).
406    #[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    /// A development instance: `localhost`, `127.0.0.1`, `[::1]`, and
413    /// `*.localhost` are answered as the site (with any port), and HTTPS is
414    /// not required for them. Never set this in production: it is the only
415    /// way a `Host: localhost` is trusted.
416    #[must_use]
417    pub fn development(mut self) -> Self {
418        self.site.development = true;
419        self
420    }
421
422    /// The path the site is installed under (`/blog`). It is removed before
423    /// routing and added back by [`Cx::url`], so routes and links are written
424    /// as if the site were at `/`. Requests outside it get 404.
425    #[must_use]
426    pub fn base(mut self, base: &str) -> Self {
427        self.base = base.to_owned();
428        self
429    }
430
431    /// The trailing-slash policy (default [`TrailingSlash::Strip`]).
432    #[must_use]
433    pub fn trailing_slash(mut self, policy: TrailingSlash) -> Self {
434        self.site.slash = policy;
435        self
436    }
437
438    /// Paths are lower case: one with an upper-case letter is redirected.
439    #[must_use]
440    pub fn lowercase_paths(mut self) -> Self {
441        self.site.lowercase = true;
442        self
443    }
444
445    /// Plain HTTP is redirected to HTTPS on the canonical host (behind a
446    /// TLS proxy, serve-net must trust the proxy's scheme). Needs a host.
447    #[must_use]
448    pub fn https_only(mut self) -> Self {
449        self.site.https_only = true;
450        self
451    }
452
453    /// The site-relative sign-in page. A [`Denial::sign_in`] on a `GET` or
454    /// `HEAD` is then answered with a 303 to it, the page asked for passed
455    /// as `next`; without it, or for other methods, the answer is 401.
456    #[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    /// The `WWW-Authenticate` challenge sent with a 401 (default
463    /// `Session`).
464    #[must_use]
465    pub fn challenge(mut self, challenge: &str) -> Self {
466        challenge.clone_into(&mut self.challenge);
467        self
468    }
469
470    /// Where refusals, denials, and failures are reported.
471    #[must_use]
472    pub fn events(mut self, events: impl Events) -> Self {
473        self.events = Box::new(events);
474        self
475    }
476
477    /// The in-flight revocation check at web-net's stages.
478    #[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    /// The budget for one rendered page (default 8 MiB).
485    #[must_use]
486    pub fn page_limit(mut self, bytes: usize) -> Self {
487        self.page_limit = bytes;
488        self
489    }
490
491    /// Zero the reply buffers web-net reuses after every request, so one
492    /// visitor's page never lingers in memory another request reuses. Costs
493    /// one pass over the bytes written.
494    #[must_use]
495    pub fn wipe_buffers(mut self) -> Self {
496        self.wipe = true;
497        self
498    }
499
500    /// Compiles the application.
501    ///
502    /// # Errors
503    ///
504    /// A [`BuildError`] listing every problem: a route without declared
505    /// access, a malformed pattern, two handlers for one method of one
506    /// path, an unknown template, a malformed host or base path.
507    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        // Site.
517        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        // Routes.
604        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    /// The template engine. Set it before adding routes and hooks: their
771    /// handlers are typed by it, so any added before are dropped and the
772    /// build says so.
773    #[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
827/// `example.com:8080` to (`example.com`, `Some("8080")`); `[::1]:80` to
828/// (`[::1]`, `Some("80")`).
829fn 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
845/// Case-insensitive, so no request needs a lower-case copy of its host.
846fn 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
855/// The authority without trailing dots, and its host without the port.
856fn host_parts(authority: &str) -> (&str, &str) {
857    let full = authority.trim_end_matches('.');
858    (full, split_port(full).0.trim_end_matches('.'))
859}
860
861/// Whether a declared host (lower case) names this authority: the whole
862/// authority, or the host when the declared one has no port.
863fn 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    /// Reply buffers reused between requests on one thread. Taken for the
871    /// length of one request and put back, so nothing about a request lives
872    /// here between calls.
873    static SCRATCH: Cell<Option<Box<Out>>> = const { Cell::new(None) };
874}
875
876/// Buffers bigger than this are dropped instead of kept.
877const KEEP_CAPACITY: usize = 1 << 20;
878
879/// Where the site check sent a request.
880enum 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    /// Runs the whole pipeline for one request, authentication included.
888    pub fn respond(&self, req: &HttpRequest<'_>, env: &Envelope, reply: &mut HttpReply) -> Outcome {
889        self.serve(req, env, None, reply).outcome
890    }
891
892    /// Runs the pipeline and reports what ran and whether a cache in front
893    /// may keep the reply for guests. `who` is `Some` when the caller
894    /// authenticated already (as for
895    /// [`respond_identified`](Self::respond_identified)).
896    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    /// Runs the pipeline for a request whose caller authenticated it
913    /// already: web-net takes `who` as given and does not call the gate's
914    /// `authenticate`. Authorization still runs for every route that is
915    /// not public.
916    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    /// Looks at a head before admission: which route would run and how it
927    /// behaves. Nothing is answered and no gate is called.
928    #[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    /// The gate.
951    #[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    /// Steps 1 and 2: which site, which canonical path. `quiet` (for
968    /// `classify`) skips building a redirect.
969    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        // With hosts declared, an empty or missing Host is as unknown as any
983        // other host.
984        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        // The path: canonical, inside the base, with the slash policy.
998        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                        // `/style.css/` and `/style.css` are one page: no slash.
1030                        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            // The host is checked as for any request: `OPTIONS *` to a host
1079            // the site does not serve is a 421 too.
1080            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        // Early: health paths and files, before the gate and the router.
1135        if let Some(outcome) = self.early(http, at, &path, reply) {
1136            return outcome;
1137        }
1138        // CORS preflights carry no credentials: answered before the gate.
1139        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        // Authentication, unless the caller did it.
1154        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        // The gate's own cookie (a dead session cleared, a pre-session id
1173        // set) leaves on every exit after authentication, once. A handler's
1174        // reply wrote it already, before the handler's own fields (so a
1175        // fresh session cookie set by the handler wins); every other exit
1176        // writes it here. A caller that authenticated itself writes its own.
1177        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    /// Steps 4 onward: route, method, authorization, CSRF, the handler.
1185    #[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        // Route and method.
1201        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        // Authorization.
1229        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        // CSRF on unsafe methods.
1266        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            // Built only with an engine when a route needs one.
1284            (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        // The handler, with its hooks, on a buffer web-net holds.
1300        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    /// A CORS preflight for a route with a policy: answered before the gate.
1322    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        // Only the methods whose routes share this policy.
1343        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    /// Health paths and static files.
1370    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            // A later part of a large file: the same file, or the end of
1405            // the connection; never another reply inside this one.
1406            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    /// 405 (or 204 for `OPTIONS`) on a path that takes `GET` and `HEAD` only.
1437    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                // A handler that sets the gate's cookie itself (a new session
1502                // after sign-in) replaces it: the gate's is not sent.
1503                if gate_cookie.is_some_and(|c| out.sets_cookie(c)) {
1504                    *gate_cookie = None;
1505                }
1506                // A reply that sets a cookie is never public, whatever the
1507                // policy or the handler said: a shared cache would give one
1508                // visitor's cookie (a pre-session id, the CSRF binding) to
1509                // every guest.
1510                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    /// A gate's denial as a response.
1609    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    /// The one function every refusal and error goes through: the status,
1683    /// the error template for it or the built-in page, the reference; the
1684    /// code and source to the events only.
1685    #[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    /// Adds the policy's security headers, except those `has` says the reply
1739    /// set already.
1740    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    /// How well this application claims a request, for [`Sites`](crate::Sites):
1749    /// the host's rank (2 listed or an alias, 1 a development host, 0 any
1750    /// host accepted) and the base path's length. `None`: not this site.
1751    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        // A site with lower-case paths claims `/BLOG/x` too: it redirects it
1765        // to `/blog/x` itself, instead of another site answering it.
1766        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    /// Whether HTTPS-only headers apply: the request came over HTTPS and is
1780    /// not for a development host.
1781    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
1806/// Whether the client prefers JSON to HTML (no `Accept`: HTML).
1807fn wants_json(accept: Option<&[u8]>) -> bool {
1808    // Most requests never name JSON: skip the parse for them.
1809    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
1813/// gzip for a reply worth compressing that the client accepts. Adds `Vary`
1814/// for every compressible reply, compressed or not, so caches keep the two
1815/// apart.
1816fn 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
1841/// Percent-encodes a query component: everything but unreserved and `/`.
1842fn 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}