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, Event, EventKind, Events, Fields, Gate, Identity, NoEvents, Open,
28    Output, Render, Request, RequestId, Result, Stage, TemplateId, Value,
29};
30
31/// The default budget for one rendered page.
32pub const DEFAULT_PAGE_LIMIT: usize = 8 << 20;
33
34/// Statuses with an error template lookup (`error/404`, ...).
35const ERROR_STATUSES: [u16; 14] = [
36    400, 401, 403, 404, 405, 409, 410, 413, 421, 422, 429, 500, 501, 503,
37];
38
39/// A named point in the pipeline where registered code runs, only for the
40/// routes it registered for. A route with no hooks pays nothing.
41#[non_exhaustive]
42#[derive(Clone, Copy, Debug, PartialEq, Eq)]
43pub enum Hook {
44    /// After the access check passed, before the handler: locale, feature
45    /// flags, an A/B variant. An error ends the request.
46    Routed,
47    /// After the handler succeeded, before the reply leaves: add headers.
48    /// An error replaces the reply with the error page.
49    Finish,
50    /// Inside [`Cx::render`], before the engine runs: choose a variant, add
51    /// headers. A hook that renders does not run these hooks again.
52    BeforeRender,
53}
54
55/// Which routes a hook runs for.
56#[derive(Clone, Debug)]
57pub struct Routes(Selector);
58
59#[derive(Clone, Debug)]
60enum Selector {
61    All,
62    Prefix(String),
63    Named(Vec<&'static str>),
64}
65
66impl Routes {
67    /// Every route.
68    #[must_use]
69    pub fn all() -> Self {
70        Self(Selector::All)
71    }
72
73    /// Routes whose pattern starts with `prefix` (`/blog`).
74    #[must_use]
75    pub fn prefix(prefix: impl Into<String>) -> Self {
76        Self(Selector::Prefix(prefix.into()))
77    }
78
79    /// Routes with one of these names.
80    #[must_use]
81    pub fn named(names: &[&'static str]) -> Self {
82        Self(Selector::Named(names.to_vec()))
83    }
84
85    fn matches(&self, pattern: &str, name: Option<&str>) -> bool {
86        match &self.0 {
87            Selector::All => true,
88            Selector::Prefix(p) => {
89                let p = p.trim_end_matches('/');
90                pattern == p
91                    || pattern
92                        .strip_prefix(p)
93                        .is_some_and(|rest| rest.starts_with('/'))
94            }
95            Selector::Named(names) => name.is_some_and(|n| names.contains(&n)),
96        }
97    }
98}
99
100pub(crate) type HookFn<I> = Arc<dyn Fn(&mut Cx<'_, I>) -> Result + Send + Sync>;
101
102/// What [`App::classify`] tells a dispatcher before admission.
103#[derive(Clone, Copy, Debug, PartialEq, Eq)]
104pub struct Classified {
105    /// The route that will run, if one matched.
106    pub route: Option<RouteId>,
107    /// How its handler behaves (`Quick` when nothing matched).
108    pub work: WorkClass,
109}
110
111/// A declared route (one method of one pattern), numbered in declaration
112/// order.
113#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
114pub struct RouteId(pub u32);
115
116/// The parts of the application every handler can reach.
117pub(crate) struct Shared {
118    pub(crate) render: Option<Box<dyn Render>>,
119    pub(crate) checkpoint: Option<Box<dyn crate::Checkpoint>>,
120    pub(crate) events: Box<dyn Events>,
121    pub(crate) page_limit: usize,
122    pub(crate) base: Box<str>,
123    errors: Vec<(u16, TemplateId)>,
124    sign_in: Option<Box<str>>,
125    challenge: Box<str>,
126    wipe: bool,
127    security: SecurityHeaders,
128    pub(crate) csrf: Option<csrf_sec::Csrf>,
129    pub(crate) redirect_hosts: Vec<Box<str>>,
130    compression: Option<Compression>,
131}
132
133/// Where a reply is going: the request's envelope, and whether security
134/// headers for HTTPS apply (HTTPS, and not a development host).
135#[derive(Clone, Copy)]
136pub(crate) struct At<'a> {
137    pub(crate) env: &'a Envelope,
138    pub(crate) tls: bool,
139    /// The client prefers JSON to HTML: error replies are JSON.
140    pub(crate) json: bool,
141}
142
143type ReadyFn = Arc<dyn Fn() -> bool + Send + Sync>;
144
145#[derive(Clone)]
146enum Health {
147    Live,
148    Ready(ReadyFn),
149}
150
151#[derive(Clone, Debug, Default)]
152struct Site {
153    /// Lower case; the first is canonical.
154    hosts: Vec<Box<str>>,
155    aliases: Vec<Box<str>>,
156    development: bool,
157    slash: TrailingSlash,
158    lowercase: bool,
159    https_only: bool,
160}
161
162struct Entry<G: Gate> {
163    handler: BoxHandler<G::Identity>,
164    access: Access<G::Rule>,
165    names: Box<[Box<str>]>,
166    pattern: Box<str>,
167    work: WorkClass,
168    template: Option<TemplateId>,
169    csrf: bool,
170    limits: form_net::Limits,
171    cors: Option<Arc<Cors>>,
172    cache: CachePolicy,
173    /// `Cache-Control` for guests and for signed-in visitors, made once.
174    cache_values: (Option<Box<str>>, Option<Box<str>>),
175    routed: Box<[HookFn<G::Identity>]>,
176    finish: Box<[HookFn<G::Identity>]>,
177    before_render: Box<[HookFn<G::Identity>]>,
178}
179
180struct Endpoint {
181    /// Entry per method, by `Method::index`.
182    methods: [Option<u32>; 10],
183    allow: Box<str>,
184    set: MethodSet,
185}
186
187/// A web application: one site, its routes, and its gate. Built once at
188/// start-up with [`App::builder`] or [`App::with_gate`]; immutable after.
189///
190/// It is a `serve_net::HttpService`, so it runs on serve-net as it is, and
191/// it has [`respond`](Self::respond) and
192/// [`respond_identified`](Self::respond_identified) for a wrapper that
193/// calls it from its own service.
194///
195/// ```
196/// use web_net::{App, get};
197///
198/// let app = App::builder()
199///     .route(get("/", |cx| { cx.html(200, "<h1>Hello</h1>"); Ok(()) }).public())
200///     .route(get("/users/:id", |cx| {
201///         let id: u64 = cx.param_as("id")?;
202///         cx.text(200, &format!("user {id}"));
203///         Ok(())
204///     }).public())
205///     .build()?;
206///
207/// let reply = serve_net::testing::respond(&app, b"GET /users/7 HTTP/1.1\r\nHost: a\r\n\r\n");
208/// assert_eq!((reply.status, reply.text()), (200, "user 7"));
209///
210/// let reply = serve_net::testing::respond(&app, b"GET /users/7/ HTTP/1.1\r\nHost: a\r\n\r\n");
211/// assert_eq!(reply.status, 301);
212/// assert_eq!(reply.header("location"), Some(&b"/users/7"[..]));
213/// # Ok::<(), web_net::BuildError>(())
214/// ```
215pub struct App<G: Gate = Open> {
216    gate: G,
217    shared: Shared,
218    site: Site,
219    trie: Trie,
220    endpoints: Vec<Endpoint>,
221    entries: Vec<Entry<G>>,
222    allow_all: Box<str>,
223    assets: Vec<Assets>,
224    health: Vec<(Box<str>, Health)>,
225}
226
227impl App<Open> {
228    /// A builder with the [`Open`] gate: every visitor is a guest and every
229    /// route is declared public.
230    #[must_use]
231    pub fn builder() -> Builder<Open> {
232        Builder::new(Open)
233    }
234}
235
236impl<G: Gate> App<G> {
237    /// A builder with `gate` for authentication and authorization.
238    #[must_use]
239    pub fn with_gate(gate: G) -> Builder<G> {
240        Builder::new(gate)
241    }
242}
243
244/// Declares an application. Every mistake is reported by
245/// [`build`](Self::build) at once, before anything listens.
246pub struct Builder<G: Gate> {
247    gate: G,
248    routes: Vec<Route<G::Identity, G::Rule>>,
249    hooks: Vec<(Hook, Routes, HookFn<G::Identity>)>,
250    site: Site,
251    base: String,
252    render: Option<Box<dyn Render>>,
253    checkpoint: Option<Box<dyn crate::Checkpoint>>,
254    events: Box<dyn Events>,
255    page_limit: usize,
256    sign_in: Option<String>,
257    challenge: String,
258    wipe: bool,
259    security: SecurityHeaders,
260    assets: Vec<Assets>,
261    health: Vec<(String, Health)>,
262    csrf: Option<csrf_sec::Csrf>,
263    redirect_hosts: Vec<Box<str>>,
264    compression: Option<Compression>,
265}
266
267impl<G: Gate> Builder<G> {
268    fn new(gate: G) -> Self {
269        Self {
270            gate,
271            routes: Vec::new(),
272            hooks: Vec::new(),
273            site: Site::default(),
274            base: String::new(),
275            render: None,
276            checkpoint: None,
277            events: Box::new(NoEvents),
278            page_limit: DEFAULT_PAGE_LIMIT,
279            sign_in: None,
280            challenge: "Session".into(),
281            wipe: false,
282            security: SecurityHeaders::default(),
283            assets: Vec::new(),
284            health: Vec::new(),
285            csrf: None,
286            redirect_hosts: Vec::new(),
287            compression: None,
288        }
289    }
290
291    /// Compress replies with gzip when the client accepts it: HTML, CSS,
292    /// JavaScript, JSON, XML, SVG, and other text, over a minimum size. Off
293    /// by default: a proxy in front usually compresses.
294    #[must_use]
295    pub fn compress(mut self, compression: Compression) -> Self {
296        self.compression = Some(compression);
297        self
298    }
299
300    /// CSRF protection with csrf-sec (re-exported as
301    /// [`web_net::csrf_sec`](crate::csrf_sec)), built from a persisted
302    /// 32-byte key. Required as soon as one route takes an unsafe method and
303    /// is not [exempt](crate::Route::csrf_exempt).
304    #[must_use]
305    pub fn csrf(mut self, csrf: csrf_sec::Csrf) -> Self {
306        self.csrf = Some(csrf);
307        self
308    }
309
310    /// Hosts that [`Cx::redirect_external`] may send a visitor to
311    /// (`accounts.example.com`). Everything else is refused.
312    #[must_use]
313    pub fn redirect_hosts(mut self, hosts: &[&str]) -> Self {
314        self.redirect_hosts.extend(
315            hosts
316                .iter()
317                .map(|h| h.to_ascii_lowercase().into_boxed_str()),
318        );
319        self
320    }
321
322    /// Serves a folder of files under a mount path, before routing and
323    /// without the gate (files are public).
324    #[must_use]
325    pub fn assets(mut self, assets: Assets) -> Self {
326        self.assets.push(assets);
327        self
328    }
329
330    /// A liveness path (`/healthz`): `200 ok` whenever the process answers.
331    /// Answered before routing and without the gate.
332    #[must_use]
333    pub fn health(mut self, path: &str) -> Self {
334        self.health.push((path.to_owned(), Health::Live));
335        self
336    }
337
338    /// A readiness path (`/readyz`): `200 ready` when `check` says so,
339    /// `503 not ready` otherwise. Keep `check` to a flag read: it runs on the
340    /// request path.
341    #[must_use]
342    pub fn ready(mut self, path: &str, check: impl Fn() -> bool + Send + Sync + 'static) -> Self {
343        self.health
344            .push((path.to_owned(), Health::Ready(Arc::new(check))));
345        self
346    }
347
348    /// The security headers every reply carries (default
349    /// [`SecurityHeaders::default`]).
350    #[must_use]
351    pub fn security_headers(mut self, policy: SecurityHeaders) -> Self {
352        self.security = policy;
353        self
354    }
355
356    /// Adds a route.
357    #[must_use]
358    pub fn route(mut self, route: Route<G::Identity, G::Rule>) -> Self {
359        self.routes.push(route);
360        self
361    }
362
363    /// Adds a group's routes.
364    #[must_use]
365    pub fn group(mut self, group: Group<G::Identity, G::Rule>) -> Self {
366        self.routes.extend(group.into_routes());
367        self
368    }
369
370    /// Runs `f` at `hook` for the routes `routes` selects.
371    #[must_use]
372    pub fn hook<F>(mut self, hook: Hook, routes: Routes, f: F) -> Self
373    where
374        F: Fn(&mut Cx<'_, G::Identity>) -> Result + Send + Sync + 'static,
375    {
376        self.hooks.push((hook, routes, Arc::new(f)));
377        self
378    }
379
380    /// A host the site answers (`example.com`, or `localhost:8080` with its
381    /// port). The first one is canonical. With no host declared the site
382    /// answers any host; with hosts declared, any other host gets 421.
383    #[must_use]
384    pub fn host(mut self, host: &str) -> Self {
385        self.site.hosts.push(host.to_ascii_lowercase().into());
386        self
387    }
388
389    /// A host that is redirected (301, or 308 for unsafe methods) to the
390    /// canonical host (`www.example.com` to `example.com`).
391    #[must_use]
392    pub fn alias(mut self, host: &str) -> Self {
393        self.site.aliases.push(host.to_ascii_lowercase().into());
394        self
395    }
396
397    /// A development instance: `localhost`, `127.0.0.1`, `[::1]`, and
398    /// `*.localhost` are answered as the site (with any port), and HTTPS is
399    /// not required for them. Never set this in production: it is the only
400    /// way a `Host: localhost` is trusted.
401    #[must_use]
402    pub fn development(mut self) -> Self {
403        self.site.development = true;
404        self
405    }
406
407    /// The path the site is installed under (`/blog`). It is removed before
408    /// routing and added back by [`Cx::url`], so routes and links are written
409    /// as if the site were at `/`. Requests outside it get 404.
410    #[must_use]
411    pub fn base(mut self, base: &str) -> Self {
412        self.base = base.to_owned();
413        self
414    }
415
416    /// The trailing-slash policy (default [`TrailingSlash::Strip`]).
417    #[must_use]
418    pub fn trailing_slash(mut self, policy: TrailingSlash) -> Self {
419        self.site.slash = policy;
420        self
421    }
422
423    /// Paths are lower case: one with an upper-case letter is redirected.
424    #[must_use]
425    pub fn lowercase_paths(mut self) -> Self {
426        self.site.lowercase = true;
427        self
428    }
429
430    /// Plain HTTP is redirected to HTTPS on the canonical host (behind a
431    /// TLS proxy, serve-net must trust the proxy's scheme). Needs a host.
432    #[must_use]
433    pub fn https_only(mut self) -> Self {
434        self.site.https_only = true;
435        self
436    }
437
438    /// The site-relative sign-in page. A [`Denial::sign_in`] on a `GET` or
439    /// `HEAD` is then answered with a 303 to it, the page asked for passed
440    /// as `next`; without it, or for other methods, the answer is 401.
441    #[must_use]
442    pub fn sign_in(mut self, path: &str) -> Self {
443        self.sign_in = Some(path.to_owned());
444        self
445    }
446
447    /// The `WWW-Authenticate` challenge sent with a 401 (default
448    /// `Session`).
449    #[must_use]
450    pub fn challenge(mut self, challenge: &str) -> Self {
451        challenge.clone_into(&mut self.challenge);
452        self
453    }
454
455    /// The template engine.
456    #[must_use]
457    pub fn render(mut self, engine: impl Render) -> Self {
458        self.render = Some(Box::new(engine));
459        self
460    }
461
462    /// Where refusals, denials, and failures are reported.
463    #[must_use]
464    pub fn events(mut self, events: impl Events) -> Self {
465        self.events = Box::new(events);
466        self
467    }
468
469    /// The in-flight revocation check at web-net's stages.
470    #[must_use]
471    pub fn checkpoint(mut self, checkpoint: impl crate::Checkpoint) -> Self {
472        self.checkpoint = Some(Box::new(checkpoint));
473        self
474    }
475
476    /// The budget for one rendered page (default 8 MiB).
477    #[must_use]
478    pub fn page_limit(mut self, bytes: usize) -> Self {
479        self.page_limit = bytes;
480        self
481    }
482
483    /// Zero the reply buffers web-net reuses after every request, so one
484    /// visitor's page never lingers in memory another request reuses. Costs
485    /// one pass over the bytes written.
486    #[must_use]
487    pub fn wipe_buffers(mut self) -> Self {
488        self.wipe = true;
489        self
490    }
491
492    /// Compiles the application.
493    ///
494    /// # Errors
495    ///
496    /// A [`BuildError`] listing every problem: a route without declared
497    /// access, a malformed pattern, two handlers for one method of one
498    /// path, an unknown template, a malformed host or base path.
499    pub fn build(self) -> std::result::Result<App<G>, BuildError> {
500        let mut problems = Vec::new();
501        let mut problem = |subject: &str, message: String| {
502            problems.push(Problem {
503                subject: subject.to_owned(),
504                message,
505            });
506        };
507
508        // Site.
509        let base = self.base.trim_end_matches('/');
510        if !base.is_empty() {
511            match url::normalize(base, self.site.lowercase) {
512                Ok(n) if n == base && !base.contains(['?', '#']) => {}
513                _ => problem(
514                    "base",
515                    format!("'{}' is not a canonical path such as '/blog'", self.base),
516                ),
517            }
518        }
519        for h in self.site.hosts.iter().chain(&self.site.aliases) {
520            if !valid_host(h) {
521                problem("host", format!("'{h}' is not a host name or address"));
522            }
523        }
524        if self.site.https_only && self.site.hosts.is_empty() {
525            problem(
526                "https_only",
527                "redirecting to HTTPS needs a declared host; add .host(\"example.com\")".into(),
528            );
529        }
530        if let Some(s) = &self.sign_in {
531            if !is_local_target(s) {
532                problem("sign_in", format!("'{s}' is not a site-relative path"));
533            }
534        }
535        if self.challenge.is_empty() || !self.challenge.bytes().all(|b| (0x20..0x7f).contains(&b)) {
536            problem("challenge", "the challenge must be visible ASCII".into());
537        }
538
539        if !self.security.valid() {
540            problem(
541                "security_headers",
542                "every header value must be visible ASCII and not empty".into(),
543            );
544        }
545        let mut assets = self.assets;
546        for a in &mut assets {
547            let subject = format!("assets {}", a.mount);
548            if a.mount.is_empty()
549                || !is_local_target(&a.mount)
550                || url::normalize(&a.mount, self.site.lowercase).map_or(true, |n| n != a.mount)
551            {
552                problem(
553                    &subject,
554                    "the mount must be a canonical path such as '/static'".into(),
555                );
556            }
557            match std::fs::canonicalize(&a.dir) {
558                Ok(root) if root.is_dir() => a.root = root,
559                _ => problem(
560                    &subject,
561                    format!("'{}' is not a folder that exists", a.dir.display()),
562                ),
563            }
564            for r in &self.routes {
565                if a.strip(&r.pattern).is_some() {
566                    problem(
567                        &r.pattern,
568                        format!(
569                            "the route is under the assets mount '{}' and can never run",
570                            a.mount
571                        ),
572                    );
573                }
574            }
575        }
576        for h in &self.redirect_hosts {
577            if !valid_host(h) {
578                problem(
579                    "redirect_hosts",
580                    format!("'{h}' is not a host name or address"),
581                );
582            }
583        }
584        let mut health = Vec::new();
585        for (p, h) in self.health {
586            if !is_local_target(&p) || url::normalize(&p, false).map_or(true, |n| n != p) {
587                problem(
588                    "health",
589                    format!("'{p}' is not a canonical site-relative path"),
590                );
591            }
592            health.push((p.into_boxed_str(), h));
593        }
594
595        // Error templates.
596        let mut errors = Vec::new();
597        if let Some(r) = &self.render {
598            for s in ERROR_STATUSES {
599                if let Some(t) = r.template(&format!("error/{s}")) {
600                    errors.push((s, t));
601                }
602            }
603        }
604
605        // Routes.
606        let mut trie = Trie::default();
607        let mut endpoints: Vec<Endpoint> = Vec::new();
608        let mut entries: Vec<Entry<G>> = Vec::new();
609        let mut all = MethodSet::EMPTY;
610        for r in self.routes {
611            let parts = match router::parse(&r.pattern) {
612                Ok(p) => p,
613                Err(e) => {
614                    problem(&r.pattern, e.message());
615                    continue;
616                }
617            };
618            if r.handlers.is_empty() {
619                problem(&r.pattern, "the route has no handler".into());
620                continue;
621            }
622            if matches!(r.access, Access::Undeclared) {
623                problem(
624                    &r.pattern,
625                    "the route declares no access; add .public() or .allow(rule)".into(),
626                );
627                continue;
628            }
629            let template = match (r.template, &self.render) {
630                (None, _) => None,
631                (Some(t), None) => {
632                    problem(
633                        &r.pattern,
634                        format!("template '{t}' is declared but no render engine is set"),
635                    );
636                    None
637                }
638                (Some(t), Some(engine)) => {
639                    let id = engine.template(t);
640                    if id.is_none() {
641                        problem(&r.pattern, format!("the engine has no template '{t}'"));
642                    }
643                    id
644                }
645            };
646            let ep = trie.insert(&parts, endpoints.len() as u32) as usize;
647            if ep == endpoints.len() {
648                endpoints.push(Endpoint {
649                    methods: [None; 10],
650                    allow: Box::from(""),
651                    set: MethodSet::EMPTY,
652                });
653            }
654            let names: Box<[Box<str>]> = router::names(&parts).into();
655            if r.csrf_exempt.is_some_and(|reason| reason.trim().is_empty()) {
656                problem(&r.pattern, "a CSRF exemption needs its reason".into());
657            }
658            if let Some(why) = r.cors.as_deref().and_then(Cors::problem) {
659                problem(&r.pattern, why.into());
660            }
661            if let Err(e) = r.form_limits.validate() {
662                problem(&r.pattern, format!("the form limits are inconsistent: {e}"));
663            }
664            for (m, h) in r.handlers {
665                let subject = format!("{} {}", m.as_str().unwrap_or("?"), r.pattern);
666                if m == Method::Other || m == Method::Connect || m == Method::Trace {
667                    problem(&subject, "this method cannot be routed".into());
668                    continue;
669                }
670                let slot = &mut endpoints[ep].methods[m.index() as usize];
671                if slot.is_some() {
672                    problem(
673                        &subject,
674                        "a handler for this method and path exists already".into(),
675                    );
676                    continue;
677                }
678                *slot = Some(entries.len() as u32);
679                let csrf = r.csrf_exempt.is_none() && !m.is_safe();
680                if csrf && self.csrf.is_none() {
681                    problem(
682                        &subject,
683                        "an unsafe method needs CSRF protection; set Builder::csrf(..) or mark the route csrf_exempt(\"reason\")".into(),
684                    );
685                }
686                let pick = |at: Hook| -> Box<[HookFn<G::Identity>]> {
687                    self.hooks
688                        .iter()
689                        .filter(|(h, sel, _)| *h == at && sel.matches(&r.pattern, r.name))
690                        .map(|(_, _, f)| Arc::clone(f))
691                        .collect()
692                };
693                entries.push(Entry {
694                    handler: h,
695                    access: r.access.clone(),
696                    names: names.clone(),
697                    pattern: r.pattern.as_str().into(),
698                    work: r.work,
699                    template,
700                    csrf,
701                    limits: r.form_limits,
702                    cors: r.cors.clone(),
703                    cache: r.cache,
704                    cache_values: (
705                        r.cache.value(true).map(String::into_boxed_str),
706                        r.cache.value(false).map(String::into_boxed_str),
707                    ),
708                    routed: pick(Hook::Routed),
709                    finish: pick(Hook::Finish),
710                    before_render: pick(Hook::BeforeRender),
711                });
712            }
713        }
714        for e in &mut endpoints {
715            let mut set = MethodSet::EMPTY;
716            for m in [
717                Method::Get,
718                Method::Head,
719                Method::Post,
720                Method::Put,
721                Method::Delete,
722                Method::Options,
723                Method::Patch,
724            ] {
725                if e.methods[m.index() as usize].is_some() {
726                    set = set.with(m);
727                }
728            }
729            if set.contains(Method::Get) {
730                set = set.with(Method::Head);
731            }
732            set = set.with(Method::Options);
733            e.set = set;
734            all = all.union(set);
735            let mut v = Vec::new();
736            set.write_allow(&mut v);
737            e.allow = String::from_utf8(v).unwrap_or_default().into();
738        }
739        if !problems.is_empty() {
740            return Err(BuildError::new(problems));
741        }
742        let mut v = Vec::new();
743        all.with(Method::Options).write_allow(&mut v);
744        Ok(App {
745            gate: self.gate,
746            shared: Shared {
747                render: self.render,
748                checkpoint: self.checkpoint,
749                events: self.events,
750                page_limit: self.page_limit,
751                base: base.into(),
752                errors,
753                sign_in: self.sign_in.map(Into::into),
754                challenge: self.challenge.into(),
755                wipe: self.wipe,
756                security: self.security,
757                csrf: self.csrf,
758                redirect_hosts: self.redirect_hosts,
759                compression: self.compression,
760            },
761            site: self.site,
762            trie,
763            endpoints,
764            entries,
765            allow_all: String::from_utf8(v).unwrap_or_default().into(),
766            assets,
767            health,
768        })
769    }
770}
771
772impl<G: Gate> fmt::Debug for Builder<G> {
773    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
774        f.debug_struct("Builder")
775            .field("routes", &self.routes)
776            .field("site", &self.site)
777            .field("base", &self.base)
778            .finish_non_exhaustive()
779    }
780}
781
782fn valid_host(h: &str) -> bool {
783    let (host, port) = split_port(h);
784    let host_ok = if let Some(inner) = host.strip_prefix('[').and_then(|r| r.strip_suffix(']')) {
785        inner.parse::<std::net::Ipv6Addr>().is_ok()
786    } else {
787        !host.is_empty()
788            && host.len() <= 253
789            && host
790                .bytes()
791                .all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'.')
792    };
793    host_ok && port.is_none_or(|p| !p.is_empty() && p.parse::<u16>().is_ok())
794}
795
796/// `example.com:8080` to (`example.com`, `Some("8080")`); `[::1]:80` to
797/// (`[::1]`, `Some("80")`).
798fn split_port(a: &str) -> (&str, Option<&str>) {
799    if a.starts_with('[') {
800        return match a.find(']') {
801            Some(i) => match a[i + 1..].strip_prefix(':') {
802                Some(p) => (&a[..=i], Some(p)),
803                None => (a, None),
804            },
805            None => (a, None),
806        };
807    }
808    match a.rsplit_once(':') {
809        Some((h, p)) => (h, Some(p)),
810        None => (a, None),
811    }
812}
813
814/// Case-insensitive, so no request needs a lower-case copy of its host.
815fn is_dev_host(host: &str) -> bool {
816    const SUFFIX: &[u8] = b".localhost";
817    let b = host.as_bytes();
818    ["localhost", "127.0.0.1", "[::1]"]
819        .iter()
820        .any(|d| d.eq_ignore_ascii_case(host))
821        || (b.len() > SUFFIX.len() && b[b.len() - SUFFIX.len()..].eq_ignore_ascii_case(SUFFIX))
822}
823
824/// The authority without trailing dots, and its host without the port.
825fn host_parts(authority: &str) -> (&str, &str) {
826    let full = authority.trim_end_matches('.');
827    (full, split_port(full).0.trim_end_matches('.'))
828}
829
830/// Whether a declared host (lower case) names this authority: the whole
831/// authority, or the host when the declared one has no port.
832fn host_listed(list: &[Box<str>], full: &str, host: &str) -> bool {
833    list.iter().any(|h| {
834        h.eq_ignore_ascii_case(full) || (split_port(h).1.is_none() && h.eq_ignore_ascii_case(host))
835    })
836}
837
838thread_local! {
839    /// Reply buffers reused between requests on one thread. Taken for the
840    /// length of one request and put back, so nothing about a request lives
841    /// here between calls.
842    static SCRATCH: Cell<Option<Box<Out>>> = const { Cell::new(None) };
843}
844
845/// Buffers bigger than this are dropped instead of kept.
846const KEEP_CAPACITY: usize = 1 << 20;
847
848/// Where the site check sent a request.
849enum Placed<'a> {
850    Here(Cow<'a, str>),
851    Redirect(String),
852    Refuse(u16, &'static str, Option<url::UrlError>),
853}
854
855impl<G: Gate> App<G> {
856    /// Runs the whole pipeline for one request, authentication included.
857    pub fn respond(&self, req: &HttpRequest<'_>, env: &Envelope, reply: &mut HttpReply) -> Outcome {
858        self.serve(req, env, None, reply).outcome
859    }
860
861    /// Runs the pipeline and reports what ran and whether a cache in front
862    /// may keep the reply for guests. `who` is `Some` when the caller
863    /// authenticated already (as for
864    /// [`respond_identified`](Self::respond_identified)).
865    pub fn serve(
866        &self,
867        req: &HttpRequest<'_>,
868        env: &Envelope,
869        who: Option<&G::Identity>,
870        reply: &mut HttpReply,
871    ) -> Served {
872        let mut report = Served {
873            outcome: Outcome::Done,
874            route: None,
875            cache: Cacheable::No,
876        };
877        report.outcome = self.run(req, env, who, reply, &mut report);
878        report
879    }
880
881    /// Runs the pipeline for a request whose caller authenticated it
882    /// already: web-net takes `who` as given and does not call the gate's
883    /// `authenticate`. Authorization still runs for every route that is
884    /// not public.
885    pub fn respond_identified(
886        &self,
887        req: &HttpRequest<'_>,
888        env: &Envelope,
889        who: &G::Identity,
890        reply: &mut HttpReply,
891    ) -> Outcome {
892        self.serve(req, env, Some(who), reply).outcome
893    }
894
895    /// Looks at a head before admission: which route would run and how it
896    /// behaves. Nothing is answered and no gate is called.
897    #[must_use]
898    pub fn classify(&self, head: &HttpHead<'_>) -> Classified {
899        let none = Classified {
900            route: None,
901            work: WorkClass::Quick,
902        };
903        let Placed::Here(path) = self.place(head.authority(), head.path(), Scheme::Https, true)
904        else {
905            return none;
906        };
907        let Some((ep, _)) = self.trie.find(&path) else {
908            return none;
909        };
910        match self.entry_for(ep as usize, head.method()) {
911            Some(i) => Classified {
912                route: Some(RouteId(i)),
913                work: self.entries[i as usize].work,
914            },
915            None => none,
916        }
917    }
918
919    /// The gate.
920    #[must_use]
921    pub fn gate(&self) -> &G {
922        &self.gate
923    }
924
925    fn entry_for(&self, ep: usize, m: Method) -> Option<u32> {
926        let methods = &self.endpoints[ep].methods;
927        match m {
928            Method::Other => None,
929            Method::Head => {
930                methods[Method::Head.index() as usize].or(methods[Method::Get.index() as usize])
931            }
932            m => methods[m.index() as usize],
933        }
934    }
935
936    /// Steps 1 and 2: which site, which canonical path. `quiet` (for
937    /// `classify`) skips building a redirect.
938    fn place<'a>(
939        &self,
940        authority: Option<&'a str>,
941        raw: &'a str,
942        scheme: Scheme,
943        quiet: bool,
944    ) -> Placed<'a> {
945        let site = &self.site;
946        let authority = authority.unwrap_or("");
947        let (full, host_only) = host_parts(authority);
948        let listed = |list: &[Box<str>]| host_listed(list, full, host_only);
949        let dev = site.development && is_dev_host(host_only);
950        let mut target_host: Option<&str> = None;
951        // With hosts declared, an empty or missing Host is as unknown as any
952        // other host.
953        if !site.hosts.is_empty() && !dev && !listed(&site.hosts) {
954            if listed(&site.aliases) {
955                target_host = site.hosts.first().map(AsRef::as_ref);
956            } else {
957                return Placed::Refuse(421, "host.unknown", None);
958            }
959        }
960        let mut https = scheme == Scheme::Https;
961        if site.https_only && !https && !dev {
962            https = true;
963            target_host = target_host.or(site.hosts.first().map(AsRef::as_ref));
964        }
965
966        // The path: canonical, inside the base, with the slash policy.
967        let canon = match url::normalize(raw, site.lowercase) {
968            Ok(c) => c,
969            Err(e) => return Placed::Refuse(400, e.code(), Some(e)),
970        };
971        let base = self.shared.base.as_ref();
972        let c: &str = &canon;
973        let rel_start = if base.is_empty() {
974            0
975        } else if c == base {
976            c.len()
977        } else if c.strip_prefix(base).is_some_and(|r| r.starts_with('/')) {
978            base.len()
979        } else {
980            return Placed::Refuse(404, "site.outside_base", None);
981        };
982        let rel = &c[rel_start..];
983        let mut fixed: Option<String> = None;
984        let health = self.health.iter().any(|(p, _)| p.as_ref() == rel);
985        if rel.is_empty() {
986            fixed = Some("/".into());
987        } else if rel.len() > 1 && !health {
988            match site.slash {
989                TrailingSlash::Strip => {
990                    if let Some(s) = rel.strip_suffix('/') {
991                        fixed = Some(s.to_owned());
992                    }
993                }
994                TrailingSlash::Add => {
995                    let trimmed = rel.strip_suffix('/').unwrap_or(rel);
996                    let file_like = trimmed.rsplit('/').next().unwrap_or("").contains('.');
997                    if file_like && rel.ends_with('/') {
998                        // `/style.css/` and `/style.css` are one page: no slash.
999                        fixed = Some(trimmed.to_owned());
1000                    } else if !file_like && !rel.ends_with('/') {
1001                        fixed = Some(format!("{rel}/"));
1002                    }
1003                }
1004                TrailingSlash::Ignore => {}
1005            }
1006        }
1007        if target_host.is_none() && fixed.is_none() {
1008            if let Cow::Borrowed(c) = canon {
1009                let mut path = &c[rel_start..];
1010                if site.slash == TrailingSlash::Ignore && path.len() > 1 {
1011                    path = path.strip_suffix('/').unwrap_or(path);
1012                }
1013                return Placed::Here(Cow::Borrowed(path));
1014            }
1015        }
1016        if quiet {
1017            return Placed::Refuse(0, "", None);
1018        }
1019        let rel_out = fixed.as_deref().unwrap_or(rel);
1020        let mut loc = String::with_capacity(raw.len() + 32);
1021        if let Some(h) = target_host {
1022            loc.push_str(if https { "https://" } else { "http://" });
1023            loc.push_str(h);
1024        }
1025        loc.push_str(base);
1026        loc.push_str(rel_out);
1027        Placed::Redirect(loc)
1028    }
1029
1030    fn run(
1031        &self,
1032        http: &HttpRequest<'_>,
1033        env: &Envelope,
1034        who: Option<&G::Identity>,
1035        reply: &mut HttpReply,
1036        report: &mut Served,
1037    ) -> Outcome {
1038        let method = http.method();
1039        let raw_path = http.path();
1040        let authority = http.head().authority();
1041        let at = At {
1042            env,
1043            tls: self.tls(env, authority),
1044            json: wants_json(http.header("accept")),
1045        };
1046        if raw_path == "*" {
1047            // The host is checked as for any request: `OPTIONS *` to a host
1048            // the site does not serve is a 421 too.
1049            if self.claim(authority, &self.shared.base).is_none() {
1050                self.refuse(
1051                    421,
1052                    "host.unknown",
1053                    EventKind::Refused,
1054                    None,
1055                    None,
1056                    at,
1057                    reply,
1058                );
1059                return Outcome::Done;
1060            }
1061            if method == Method::Options {
1062                reply.status(204).header("allow", self.allow_all.as_bytes());
1063                self.secure(reply, at.tls, |_| false);
1064            } else {
1065                self.refuse(
1066                    400,
1067                    "url.asterisk",
1068                    EventKind::Refused,
1069                    None,
1070                    None,
1071                    at,
1072                    reply,
1073                );
1074            }
1075            return Outcome::Done;
1076        }
1077        let host_raw = authority.unwrap_or("");
1078        let placed = self.place(authority, raw_path, env.client().scheme(), false);
1079        let path = match placed {
1080            Placed::Here(path) => path,
1081            Placed::Redirect(mut loc) => {
1082                if let Some(q) = http.query() {
1083                    loc.push('?');
1084                    loc.push_str(q);
1085                }
1086                let status = if matches!(method, Method::Get | Method::Head) {
1087                    301
1088                } else {
1089                    308
1090                };
1091                reply.status(status).header("location", loc.as_bytes());
1092                self.secure(reply, at.tls, |_| false);
1093                return Outcome::Done;
1094            }
1095            Placed::Refuse(status, code, src) => {
1096                let src = src
1097                    .as_ref()
1098                    .map(|e| e as &(dyn std::error::Error + 'static));
1099                self.refuse(status, code, EventKind::Refused, None, src, at, reply);
1100                return Outcome::Done;
1101            }
1102        };
1103        // Early: health paths and files, before the gate and the router.
1104        if let Some(outcome) = self.early(http, at, &path, reply) {
1105            return outcome;
1106        }
1107        // CORS preflights carry no credentials: answered before the gate.
1108        if let Some(outcome) = self.preflight(http, at, &path, reply) {
1109            return outcome;
1110        }
1111
1112        let req = Request {
1113            http,
1114            env,
1115            path: &path,
1116            host: host_raw,
1117            route: None,
1118            tls: at.tls,
1119            json: at.json,
1120        };
1121
1122        // Authentication, unless the caller did it.
1123        let owned;
1124        let owned_here = who.is_none();
1125        let who: &G::Identity = match who {
1126            Some(w) => w,
1127            None => match self.gate.authenticate(&req) {
1128                Ok(w) => {
1129                    owned = w;
1130                    &owned
1131                }
1132                Err(d) => {
1133                    self.deny(d, &req, reply);
1134                    return Outcome::Done;
1135                }
1136            },
1137        };
1138
1139        let mut gate_cookie = if owned_here { who.set_cookie() } else { None };
1140        let outcome = self.routed(http, env, at, req, who, &mut gate_cookie, reply, report);
1141        // The gate's own cookie (a dead session cleared, a pre-session id
1142        // set) leaves on every exit after authentication, once. A handler's
1143        // reply wrote it already, before the handler's own fields (so a
1144        // fresh session cookie set by the handler wins); every other exit
1145        // writes it here. A caller that authenticated itself writes its own.
1146        if let Some(c) = gate_cookie {
1147            reply.header("set-cookie", c.as_bytes());
1148            report.cache = Cacheable::No;
1149        }
1150        outcome
1151    }
1152
1153    /// Steps 4 onward: route, method, authorization, CSRF, the handler.
1154    #[allow(clippy::too_many_arguments)]
1155    fn routed<'r>(
1156        &'r self,
1157        http: &HttpRequest<'_>,
1158        env: &Envelope,
1159        at: At<'_>,
1160        mut req: Request<'r>,
1161        who: &G::Identity,
1162        gate_cookie: &mut Option<&str>,
1163        reply: &mut HttpReply,
1164        report: &mut Served,
1165    ) -> Outcome {
1166        let method = http.method();
1167        let path = req.path;
1168        let host_raw = req.host;
1169        // Route and method.
1170        let Some((ep, caps)) = self.trie.find(path) else {
1171            self.refuse(404, "route.none", EventKind::Refused, None, None, at, reply);
1172            return Outcome::Done;
1173        };
1174        let endpoint = &self.endpoints[ep as usize];
1175        let Some(idx) = self.entry_for(ep as usize, method) else {
1176            if method == Method::Options {
1177                reply.status(204).header("allow", endpoint.allow.as_bytes());
1178                self.secure(reply, at.tls, |_| false);
1179                return Outcome::Done;
1180            }
1181            let status = if method == Method::Other { 501 } else { 405 };
1182            let code = if status == 501 {
1183                "method.unknown"
1184            } else {
1185                "method.not_allowed"
1186            };
1187            self.refuse(status, code, EventKind::Refused, None, None, at, reply);
1188            if status == 405 {
1189                reply.header("allow", endpoint.allow.as_bytes());
1190            }
1191            return Outcome::Done;
1192        };
1193        let entry = &self.entries[idx as usize];
1194        req.route = Some(&entry.pattern);
1195        report.route = Some(RouteId(idx));
1196
1197        // Authorization.
1198        if let Access::Rule(rule) = &entry.access {
1199            if let Err(d) = self.gate.authorize(who, rule, &req) {
1200                self.deny(d, &req, reply);
1201                return Outcome::Done;
1202            }
1203        }
1204        if let Some(cp) = &self.shared.checkpoint {
1205            match cp.check(env, Stage::Authorized) {
1206                Verdict::Pass => {}
1207                Verdict::Deny { status } => {
1208                    self.refuse(
1209                        status,
1210                        "checkpoint.denied",
1211                        EventKind::Revoked,
1212                        req.route,
1213                        None,
1214                        at,
1215                        reply,
1216                    );
1217                    return Outcome::Done;
1218                }
1219                _ => {
1220                    self.refuse(
1221                        403,
1222                        "checkpoint.killed",
1223                        EventKind::Revoked,
1224                        req.route,
1225                        None,
1226                        at,
1227                        reply,
1228                    );
1229                    return Outcome::Close;
1230                }
1231            }
1232        }
1233
1234        // CSRF on unsafe methods.
1235        let binding = who.csrf_binding();
1236        match (&self.shared.csrf, entry.csrf) {
1237            (Some(engine), true) => {
1238                let scheme = if env.client().scheme() == Scheme::Https {
1239                    "https://"
1240                } else {
1241                    "http://"
1242                };
1243                let own = format!("{scheme}{}", host_raw.to_ascii_lowercase());
1244                match csrf::check(engine, http, binding, &own, &entry.limits) {
1245                    Ok(()) => {}
1246                    Err(code) => {
1247                        self.refuse(403, code, EventKind::Denied, req.route, None, at, reply);
1248                        return Outcome::Done;
1249                    }
1250                }
1251            }
1252            // Built only with an engine when a route needs one.
1253            (None, true) => {
1254                self.refuse(
1255                    500,
1256                    "csrf.no_engine",
1257                    EventKind::Failed,
1258                    req.route,
1259                    None,
1260                    at,
1261                    reply,
1262                );
1263                return Outcome::Done;
1264            }
1265            (_, false) => {}
1266        }
1267
1268        // The handler, with its hooks, on a buffer web-net holds.
1269        let mut out = SCRATCH.with(Cell::take).unwrap_or_default();
1270        out.reset();
1271        let outcome = self.handle(
1272            entry,
1273            req,
1274            who,
1275            caps,
1276            (gate_cookie, binding),
1277            &mut out,
1278            reply,
1279            &mut report.cache,
1280        );
1281        if self.shared.wipe {
1282            out.wipe();
1283        }
1284        if out.body.capacity() <= KEEP_CAPACITY && out.values.capacity() <= KEEP_CAPACITY {
1285            SCRATCH.with(|s| s.set(Some(out)));
1286        }
1287        outcome
1288    }
1289
1290    /// A CORS preflight for a route with a policy: answered before the gate.
1291    fn preflight(
1292        &self,
1293        http: &HttpRequest<'_>,
1294        at: At<'_>,
1295        path: &str,
1296        reply: &mut HttpReply,
1297    ) -> Option<Outcome> {
1298        if http.method() != Method::Options {
1299            return None;
1300        }
1301        let origin = http.header("origin")?;
1302        let wanted = Method::from_bytes(http.header("access-control-request-method")?);
1303        let (ep, _) = self.trie.find(path)?;
1304        let ep = ep as usize;
1305        if self.entry_for(ep, Method::Options).is_some() {
1306            return None;
1307        }
1308        let cors = self
1309            .entry_for(ep, wanted)
1310            .and_then(|i| self.entries[i as usize].cors.as_ref())?;
1311        // Only the methods whose routes share this policy.
1312        let mut set = MethodSet::EMPTY;
1313        for m in self.endpoints[ep].set.iter() {
1314            let shares = self.entry_for(ep, m).is_some_and(|i| {
1315                self.entries[i as usize]
1316                    .cors
1317                    .as_ref()
1318                    .is_some_and(|c| Arc::ptr_eq(c, cors))
1319            });
1320            if shares {
1321                set = set.with(m);
1322            }
1323        }
1324        let ok = cors.preflight(
1325            origin,
1326            wanted,
1327            http.header("access-control-request-headers"),
1328            set,
1329            reply,
1330        );
1331        if !ok {
1332            self.event(EventKind::Refused, at.env, None, 204, "cors.denied", None);
1333        }
1334        self.secure(reply, at.tls, |_| false);
1335        Some(Outcome::Done)
1336    }
1337
1338    /// Health paths and static files.
1339    fn early(
1340        &self,
1341        http: &HttpRequest<'_>,
1342        at: At<'_>,
1343        path: &str,
1344        reply: &mut HttpReply,
1345    ) -> Option<Outcome> {
1346        let method = http.method();
1347        let get_like = matches!(method, Method::Get | Method::Head);
1348        if let Some((_, h)) = self.health.iter().find(|(p, _)| p.as_ref() == path) {
1349            if !get_like {
1350                self.not_get(method, at, reply);
1351                return Some(Outcome::Done);
1352            }
1353            let (status, text) = match h {
1354                Health::Live => (200, "ok"),
1355                Health::Ready(f) if f() => (200, "ready"),
1356                Health::Ready(_) => (503, "not ready"),
1357            };
1358            reply.text(status, text).header("cache-control", "no-store");
1359            self.secure(reply, at.tls, |_| false);
1360            return Some(Outcome::Done);
1361        }
1362        let (assets, rel) = self
1363            .assets
1364            .iter()
1365            .find_map(|a| a.strip(path).map(|rel| (a, rel)))?;
1366        if !get_like {
1367            self.not_get(method, at, reply);
1368            return Some(Outcome::Done);
1369        }
1370        let found = assets::find(assets, rel, http.header("accept-encoding"));
1371        let key = (at.env.connection().get(), at.env.seq().get());
1372        if let Some(tag) = assets::continuing(key) {
1373            // A later part of a large file: the same file, or the end of
1374            // the connection; never another reply inside this one.
1375            return Some(assets::next_part(key, &tag, found.as_ref(), http, reply));
1376        }
1377        let Some(found) = found else {
1378            self.refuse(
1379                404,
1380                "assets.none",
1381                EventKind::Refused,
1382                None,
1383                None,
1384                at,
1385                reply,
1386            );
1387            return Some(Outcome::Done);
1388        };
1389        let Some(outcome) = assets::serve(assets, &found, key, http, reply) else {
1390            self.refuse(
1391                500,
1392                "assets.unreadable",
1393                EventKind::Failed,
1394                None,
1395                None,
1396                at,
1397                reply,
1398            );
1399            return Some(Outcome::Done);
1400        };
1401        self.secure(reply, at.tls, |_| false);
1402        Some(outcome)
1403    }
1404
1405    /// 405 (or 204 for `OPTIONS`) on a path that takes `GET` and `HEAD` only.
1406    fn not_get(&self, method: Method, at: At<'_>, reply: &mut HttpReply) {
1407        if method == Method::Options {
1408            reply.status(204).header("allow", "GET, HEAD, OPTIONS");
1409            self.secure(reply, at.tls, |_| false);
1410        } else {
1411            let (status, code) = if method == Method::Other {
1412                (501, "method.unknown")
1413            } else {
1414                (405, "method.not_allowed")
1415            };
1416            self.refuse(status, code, EventKind::Refused, None, None, at, reply);
1417            if status == 405 {
1418                reply.header("allow", "GET, HEAD, OPTIONS");
1419            }
1420        }
1421    }
1422
1423    #[allow(clippy::too_many_arguments)]
1424    fn handle(
1425        &self,
1426        entry: &Entry<G>,
1427        req: Request<'_>,
1428        who: &G::Identity,
1429        caps: Captures,
1430        (gate_cookie, binding): (&mut Option<&str>, Option<&[u8]>),
1431        out: &mut Out,
1432        reply: &mut HttpReply,
1433        cacheable: &mut Cacheable,
1434    ) -> Outcome {
1435        let at = At {
1436            env: req.env,
1437            tls: req.tls,
1438            json: req.json,
1439        };
1440        let route = req.route;
1441        let http = req.http;
1442        let guest = who.is_guest();
1443        let result = {
1444            let mut cx = Cx {
1445                req,
1446                who,
1447                caps,
1448                names: &entry.names,
1449                out: &mut *out,
1450                shared: &self.shared,
1451                template: entry.template,
1452                binding,
1453                limits: &entry.limits,
1454                before_render: &entry.before_render,
1455                rendering: false,
1456            };
1457            catch_unwind(AssertUnwindSafe(|| -> Result {
1458                for h in entry.routed.iter() {
1459                    h(&mut cx)?;
1460                }
1461                entry.handler.call(&mut cx)?;
1462                for h in entry.finish.iter() {
1463                    h(&mut cx)?;
1464                }
1465                Ok(())
1466            }))
1467        };
1468        match result {
1469            Ok(Ok(())) if !out.invalid => {
1470                // A handler that sets the gate's cookie itself (a new session
1471                // after sign-in) replaces it: the gate's is not sent.
1472                if gate_cookie.is_some_and(|c| out.sets_cookie(c)) {
1473                    *gate_cookie = None;
1474                }
1475                // A reply that sets a cookie is never public, whatever the
1476                // policy or the handler said: a shared cache would give one
1477                // visitor's cookie (a pre-session id, the CSRF binding) to
1478                // every guest.
1479                let cookies = gate_cookie.is_some() || out.has("set-cookie");
1480                let ours = !out.has("cache-control");
1481                if cookies {
1482                    let public = out
1483                        .get("cache-control")
1484                        .is_some_and(|v| v.windows(6).any(|w| w.eq_ignore_ascii_case(b"public")));
1485                    if ours || public {
1486                        out.remove("cache-control");
1487                        out.header("cache-control", b"private, no-store");
1488                    }
1489                } else if ours {
1490                    let (for_guest, for_member) = &entry.cache_values;
1491                    let value = if guest { for_guest } else { for_member };
1492                    if let Some(v) = value {
1493                        out.header("cache-control", v.as_bytes());
1494                    }
1495                }
1496                if let Some(c) = self.shared.compression {
1497                    compress(c, http.header("accept-encoding"), out);
1498                }
1499                *cacheable = match entry.cache {
1500                    CachePolicy::Guests { max_age } | CachePolicy::Public { max_age }
1501                        if guest
1502                            && ours
1503                            && !cookies
1504                            && entry.cors.is_none()
1505                            && matches!(http.method(), Method::Get | Method::Head)
1506                            && out.status == 200
1507                            && out
1508                                .get("vary")
1509                                .is_none_or(|v| v.eq_ignore_ascii_case(b"accept-encoding")) =>
1510                    {
1511                        Cacheable::Guests { max_age }
1512                    }
1513                    _ => Cacheable::No,
1514                };
1515                reply.status(out.status);
1516                if let Some(c) = gate_cookie.take() {
1517                    reply.header("set-cookie", c.as_bytes());
1518                }
1519                for &(ns, ne, vs, ve) in &out.spans {
1520                    let name = core::str::from_utf8(&out.names[ns as usize..ne as usize])
1521                        .unwrap_or("x-invalid");
1522                    reply.header(name, &out.values[vs as usize..ve as usize]);
1523                }
1524                if !out.has("x-content-type-options") {
1525                    reply.header("x-content-type-options", "nosniff");
1526                }
1527                if let Some(cors) = &entry.cors {
1528                    cors.actual(http.header("origin"), reply);
1529                }
1530                self.secure(reply, at.tls, |n| out.has(n));
1531                reply.body(&out.body);
1532                Outcome::Done
1533            }
1534            Ok(Ok(())) => {
1535                out.reset();
1536                self.refuse(
1537                    500,
1538                    "reply.invalid_field",
1539                    EventKind::Failed,
1540                    route,
1541                    None,
1542                    at,
1543                    reply,
1544                );
1545                Outcome::Done
1546            }
1547            Ok(Err(e)) => {
1548                out.reset();
1549                let src = std::error::Error::source(&e);
1550                self.refuse(
1551                    e.status(),
1552                    e.code(),
1553                    EventKind::Failed,
1554                    route,
1555                    src,
1556                    at,
1557                    reply,
1558                );
1559                Outcome::Done
1560            }
1561            Err(_) => {
1562                out.reset();
1563                self.refuse(
1564                    500,
1565                    "handler.panic",
1566                    EventKind::Panicked,
1567                    route,
1568                    None,
1569                    at,
1570                    reply,
1571                );
1572                Outcome::Done
1573            }
1574        }
1575    }
1576
1577    /// A gate's denial as a response.
1578    fn deny(&self, d: Denial, req: &Request<'_>, reply: &mut HttpReply) {
1579        let env = req.env;
1580        let at = At {
1581            env,
1582            tls: req.tls,
1583            json: req.json,
1584        };
1585        if d.kind() == DenialKind::SignIn {
1586            if let Some(page) = &self.shared.sign_in {
1587                if matches!(req.method(), Method::Get | Method::Head) {
1588                    let mut loc = String::with_capacity(64);
1589                    loc.push_str(&self.shared.base);
1590                    loc.push_str(page);
1591                    loc.push_str(if page.contains('?') {
1592                        "&next="
1593                    } else {
1594                        "?next="
1595                    });
1596                    let mut next = String::new();
1597                    next.push_str(&self.shared.base);
1598                    next.push_str(req.path);
1599                    if let Some(q) = req.query() {
1600                        next.push('?');
1601                        next.push_str(q);
1602                    }
1603                    encode_component(&next, &mut loc);
1604                    self.event(EventKind::Denied, env, req.route, 303, d.code(), None);
1605                    reply.status(303).header("location", loc.as_bytes());
1606                    self.secure(reply, at.tls, |_| false);
1607                    if let Some(c) = d.cookie() {
1608                        reply.header("set-cookie", c.as_bytes());
1609                    }
1610                    return;
1611                }
1612            }
1613        }
1614        let status = d.status_code();
1615        self.refuse(
1616            status,
1617            d.code(),
1618            EventKind::Denied,
1619            req.route,
1620            None,
1621            at,
1622            reply,
1623        );
1624        if status == 401 {
1625            reply.header("www-authenticate", self.shared.challenge.as_bytes());
1626        }
1627        if let Some(c) = d.cookie() {
1628            reply.header("set-cookie", c.as_bytes());
1629        }
1630    }
1631
1632    fn event(
1633        &self,
1634        kind: EventKind,
1635        env: &Envelope,
1636        route: Option<&str>,
1637        status: u16,
1638        code: &'static str,
1639        source: Option<&(dyn std::error::Error + 'static)>,
1640    ) {
1641        self.shared.events.event(&Event {
1642            kind,
1643            id: RequestId::of(env),
1644            route,
1645            status,
1646            code,
1647            source,
1648        });
1649    }
1650
1651    /// The one function every refusal and error goes through: the status,
1652    /// the error template for it or the built-in page, the reference; the
1653    /// code and source to the events only.
1654    #[allow(clippy::too_many_arguments)]
1655    fn refuse(
1656        &self,
1657        status: u16,
1658        code: &'static str,
1659        kind: EventKind,
1660        route: Option<&str>,
1661        source: Option<&(dyn std::error::Error + 'static)>,
1662        at: At<'_>,
1663        reply: &mut HttpReply,
1664    ) {
1665        self.event(kind, at.env, route, status, code, source);
1666        let reference = RequestId::of(at.env).to_string();
1667        let mut body = Vec::with_capacity(512);
1668        if at.json {
1669            error::json_body(status, &reference, &mut body);
1670            reply
1671                .status(status)
1672                .header("content-type", "application/json")
1673                .header("x-content-type-options", "nosniff")
1674                .header("cache-control", "no-store")
1675                .body(&body);
1676            self.secure(reply, at.tls, |_| false);
1677            return;
1678        }
1679        let mut done = false;
1680        if let (Some(engine), Some(&(_, t))) = (
1681            self.shared.render.as_deref(),
1682            self.shared.errors.iter().find(|(s, _)| *s == status),
1683        ) {
1684            let fields = [
1685                ("status", Value::Int(i64::from(status))),
1686                ("title", Value::Text(error::title(status))),
1687                ("message", Value::Text(error::message(status))),
1688                ("reference", Value::Text(&reference)),
1689            ];
1690            let view = Fields(&fields);
1691            let mut out = Output::new(&mut body, self.shared.page_limit);
1692            if engine.render(t, &view, &mut out).is_ok() {
1693                done = true;
1694            } else {
1695                body.clear();
1696            }
1697        }
1698        if !done {
1699            error::builtin_page(status, &reference, &mut body);
1700        }
1701        reply
1702            .status(status)
1703            .header("content-type", "text/html; charset=utf-8")
1704            .header("x-content-type-options", "nosniff")
1705            .header("cache-control", "no-store")
1706            .body(&body);
1707        self.secure(reply, at.tls, |_| false);
1708    }
1709
1710    /// Adds the policy's security headers, except those `has` says the reply
1711    /// set already.
1712    fn secure(&self, reply: &mut HttpReply, tls: bool, has: impl Fn(&str) -> bool) {
1713        for (name, value) in self.shared.security.fields(tls) {
1714            if !has(name) {
1715                reply.header(name, value.as_bytes());
1716            }
1717        }
1718    }
1719
1720    /// How well this application claims a request, for [`Sites`](crate::Sites):
1721    /// the host's rank (2 listed or an alias, 1 a development host, 0 any
1722    /// host accepted) and the base path's length. `None`: not this site.
1723    pub(crate) fn claim(&self, authority: Option<&str>, path: &str) -> Option<(u8, usize)> {
1724        let (full, host_only) = host_parts(authority.unwrap_or(""));
1725        let listed = |list: &[Box<str>]| host_listed(list, full, host_only);
1726        let rank = if self.site.hosts.is_empty() {
1727            0
1728        } else if listed(&self.site.hosts) || listed(&self.site.aliases) {
1729            2
1730        } else if self.site.development && is_dev_host(host_only) {
1731            1
1732        } else {
1733            return None;
1734        };
1735        let base = self.shared.base.as_ref();
1736        let inside = base.is_empty()
1737            || path == base
1738            || path.strip_prefix(base).is_some_and(|r| r.starts_with('/'));
1739        inside.then_some((rank, base.len()))
1740    }
1741
1742    /// Whether HTTPS-only headers apply: the request came over HTTPS and is
1743    /// not for a development host.
1744    fn tls(&self, env: &Envelope, authority: Option<&str>) -> bool {
1745        if env.client().scheme() != Scheme::Https {
1746            return false;
1747        }
1748        let (_, host) = host_parts(authority.unwrap_or(""));
1749        !(self.site.development && is_dev_host(host))
1750    }
1751}
1752
1753impl<G: Gate> HttpService for App<G> {
1754    fn call(&self, request: &HttpRequest<'_>, env: &Envelope, reply: &mut HttpReply) -> Outcome {
1755        self.respond(request, env, reply)
1756    }
1757}
1758
1759impl<G: Gate> fmt::Debug for App<G> {
1760    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1761        f.debug_struct("App")
1762            .field("site", &self.site)
1763            .field("base", &self.shared.base)
1764            .field("routes", &self.entries.len())
1765            .finish_non_exhaustive()
1766    }
1767}
1768
1769/// Whether the client prefers JSON to HTML (no `Accept`: HTML).
1770fn wants_json(accept: Option<&[u8]>) -> bool {
1771    // Most requests never name JSON: skip the parse for them.
1772    accept.is_some_and(|a| a.windows(4).any(|w| w.eq_ignore_ascii_case(b"json")))
1773        && parse_net::best_media(accept, &["text/html", "application/json"]) == Some(1)
1774}
1775
1776/// gzip for a reply worth compressing that the client accepts. Adds `Vary`
1777/// for every compressible reply, compressed or not, so caches keep the two
1778/// apart.
1779fn compress(c: Compression, accept_encoding: Option<&[u8]>, out: &mut Out) {
1780    if out.status != 200 || out.has("content-encoding") {
1781        return;
1782    }
1783    if !out.get("content-type").is_some_and(compressible) {
1784        return;
1785    }
1786    if !out.has("vary") {
1787        out.header("vary", b"accept-encoding");
1788    }
1789    let accepts = accept_encoding
1790        .and_then(|v| parse_net::best_encoding(Some(v), &["gzip", "identity"]))
1791        == Some(0);
1792    if !accepts || out.body.len() < c.min_bytes {
1793        return;
1794    }
1795    let level = compress_lib::Level::new(c.level).unwrap_or(compress_lib::Level::DEFAULT);
1796    let packed = compress_lib::gzip::compress_with(&out.body, level);
1797    if packed.len() < out.body.len() {
1798        out.body.clear();
1799        out.body.extend_from_slice(&packed);
1800        out.header("content-encoding", b"gzip");
1801    }
1802}
1803
1804/// Percent-encodes a query component: everything but unreserved and `/`.
1805fn encode_component(s: &str, out: &mut String) {
1806    const HEX: &[u8; 16] = b"0123456789ABCDEF";
1807    for b in s.bytes() {
1808        if b.is_ascii_alphanumeric() || matches!(b, b'-' | b'.' | b'_' | b'~' | b'/') {
1809            out.push(b as char);
1810        } else {
1811            out.push('%');
1812            out.push(HEX[(b >> 4) as usize] as char);
1813            out.push(HEX[(b & 15) as usize] as char);
1814        }
1815    }
1816}
1817
1818#[cfg(test)]
1819mod tests {
1820    use super::*;
1821
1822    #[test]
1823    fn test_split_port() {
1824        assert_eq!(split_port("a.test:80"), ("a.test", Some("80")));
1825        assert_eq!(split_port("a.test"), ("a.test", None));
1826        assert_eq!(split_port("[::1]:8080"), ("[::1]", Some("8080")));
1827        assert_eq!(split_port("[::1]"), ("[::1]", None));
1828    }
1829
1830    #[test]
1831    fn test_valid_host() {
1832        for ok in ["example.com", "localhost:8080", "[::1]", "127.0.0.1:80"] {
1833            assert!(valid_host(ok), "{ok}");
1834        }
1835        for bad in ["", "a b", "a:", "a:x", "[zz]", "a/b"] {
1836            assert!(!valid_host(bad), "{bad}");
1837        }
1838    }
1839
1840    #[test]
1841    fn test_selector() {
1842        assert!(Routes::prefix("/blog").matches("/blog", None));
1843        assert!(Routes::prefix("/blog/").matches("/blog/:slug", None));
1844        assert!(!Routes::prefix("/blog").matches("/blogger", None));
1845        assert!(Routes::named(&["a"]).matches("/x", Some("a")));
1846        assert!(!Routes::named(&["a"]).matches("/x", None));
1847    }
1848}