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        if !site.hosts.is_empty() && !dev && !authority.is_empty() && !listed(&site.hosts) {
952            if listed(&site.aliases) {
953                target_host = site.hosts.first().map(AsRef::as_ref);
954            } else {
955                return Placed::Refuse(421, "host.unknown", None);
956            }
957        }
958        let mut https = scheme == Scheme::Https;
959        if site.https_only && !https && !dev {
960            https = true;
961            target_host = target_host.or(site.hosts.first().map(AsRef::as_ref));
962        }
963
964        // The path: canonical, inside the base, with the slash policy.
965        let canon = match url::normalize(raw, site.lowercase) {
966            Ok(c) => c,
967            Err(e) => return Placed::Refuse(400, e.code(), Some(e)),
968        };
969        let base = self.shared.base.as_ref();
970        let c: &str = &canon;
971        let rel_start = if base.is_empty() {
972            0
973        } else if c == base {
974            c.len()
975        } else if c.strip_prefix(base).is_some_and(|r| r.starts_with('/')) {
976            base.len()
977        } else {
978            return Placed::Refuse(404, "site.outside_base", None);
979        };
980        let rel = &c[rel_start..];
981        let mut fixed: Option<String> = None;
982        if rel.is_empty() {
983            fixed = Some("/".into());
984        } else if rel.len() > 1 {
985            match site.slash {
986                TrailingSlash::Strip => {
987                    if let Some(s) = rel.strip_suffix('/') {
988                        fixed = Some(s.to_owned());
989                    }
990                }
991                TrailingSlash::Add => {
992                    let last = rel.rsplit('/').next().unwrap_or("");
993                    if !rel.ends_with('/') && !last.contains('.') {
994                        fixed = Some(format!("{rel}/"));
995                    }
996                }
997                TrailingSlash::Ignore => {}
998            }
999        }
1000        if target_host.is_none() && fixed.is_none() {
1001            if let Cow::Borrowed(c) = canon {
1002                let mut path = &c[rel_start..];
1003                if site.slash == TrailingSlash::Ignore && path.len() > 1 {
1004                    path = path.strip_suffix('/').unwrap_or(path);
1005                }
1006                return Placed::Here(Cow::Borrowed(path));
1007            }
1008        }
1009        if quiet {
1010            return Placed::Refuse(0, "", None);
1011        }
1012        let rel_out = fixed.as_deref().unwrap_or(rel);
1013        let mut loc = String::with_capacity(raw.len() + 32);
1014        if let Some(h) = target_host {
1015            loc.push_str(if https { "https://" } else { "http://" });
1016            loc.push_str(h);
1017        }
1018        loc.push_str(base);
1019        loc.push_str(rel_out);
1020        Placed::Redirect(loc)
1021    }
1022
1023    fn run(
1024        &self,
1025        http: &HttpRequest<'_>,
1026        env: &Envelope,
1027        who: Option<&G::Identity>,
1028        reply: &mut HttpReply,
1029        report: &mut Served,
1030    ) -> Outcome {
1031        let method = http.method();
1032        let raw_path = http.path();
1033        let authority = http.head().authority();
1034        let at = At {
1035            env,
1036            tls: self.tls(env, authority),
1037            json: wants_json(http.header("accept")),
1038        };
1039        if raw_path == "*" {
1040            if method == Method::Options {
1041                reply.status(204).header("allow", self.allow_all.as_bytes());
1042                self.secure(reply, at.tls, |_| false);
1043            } else {
1044                self.refuse(
1045                    400,
1046                    "url.asterisk",
1047                    EventKind::Refused,
1048                    None,
1049                    None,
1050                    at,
1051                    reply,
1052                );
1053            }
1054            return Outcome::Done;
1055        }
1056        let host_raw = authority.unwrap_or("");
1057        let placed = self.place(authority, raw_path, env.client().scheme(), false);
1058        let path = match placed {
1059            Placed::Here(path) => path,
1060            Placed::Redirect(mut loc) => {
1061                if let Some(q) = http.query() {
1062                    loc.push('?');
1063                    loc.push_str(q);
1064                }
1065                let status = if matches!(method, Method::Get | Method::Head) {
1066                    301
1067                } else {
1068                    308
1069                };
1070                reply.status(status).header("location", loc.as_bytes());
1071                self.secure(reply, at.tls, |_| false);
1072                return Outcome::Done;
1073            }
1074            Placed::Refuse(status, code, src) => {
1075                let src = src
1076                    .as_ref()
1077                    .map(|e| e as &(dyn std::error::Error + 'static));
1078                self.refuse(status, code, EventKind::Refused, None, src, at, reply);
1079                return Outcome::Done;
1080            }
1081        };
1082        // Early: health paths and files, before the gate and the router.
1083        if let Some(outcome) = self.early(http, at, &path, reply) {
1084            return outcome;
1085        }
1086
1087        let mut req = Request {
1088            http,
1089            env,
1090            path: &path,
1091            host: host_raw,
1092            route: None,
1093            tls: at.tls,
1094            json: at.json,
1095        };
1096
1097        // Authentication, unless the caller did it.
1098        let owned;
1099        let who: &G::Identity = match who {
1100            Some(w) => w,
1101            None => match self.gate.authenticate(&req) {
1102                Ok(w) => {
1103                    owned = w;
1104                    &owned
1105                }
1106                Err(d) => {
1107                    self.deny(d, &req, reply);
1108                    return Outcome::Done;
1109                }
1110            },
1111        };
1112
1113        // Route and method.
1114        let Some((ep, caps)) = self.trie.find(&path) else {
1115            self.refuse(404, "route.none", EventKind::Refused, None, None, at, reply);
1116            return Outcome::Done;
1117        };
1118        let endpoint = &self.endpoints[ep as usize];
1119        let Some(idx) = self.entry_for(ep as usize, method) else {
1120            if method == Method::Options {
1121                if let (Some(origin), Some(wanted)) = (
1122                    http.header("origin"),
1123                    http.header("access-control-request-method"),
1124                ) {
1125                    let wanted = Method::from_bytes(wanted);
1126                    let cors = self
1127                        .entry_for(ep as usize, wanted)
1128                        .and_then(|i| self.entries[i as usize].cors.as_deref());
1129                    if let Some(cors) = cors {
1130                        let ok = cors.preflight(
1131                            origin,
1132                            wanted,
1133                            http.header("access-control-request-headers"),
1134                            endpoint.set,
1135                            reply,
1136                        );
1137                        if !ok {
1138                            self.event(EventKind::Refused, env, None, 204, "cors.denied", None);
1139                        }
1140                        self.secure(reply, at.tls, |_| false);
1141                        return Outcome::Done;
1142                    }
1143                }
1144                reply.status(204).header("allow", endpoint.allow.as_bytes());
1145                self.secure(reply, at.tls, |_| false);
1146                return Outcome::Done;
1147            }
1148            let status = if method == Method::Other { 501 } else { 405 };
1149            let code = if status == 501 {
1150                "method.unknown"
1151            } else {
1152                "method.not_allowed"
1153            };
1154            self.refuse(status, code, EventKind::Refused, None, None, at, reply);
1155            if status == 405 {
1156                reply.header("allow", endpoint.allow.as_bytes());
1157            }
1158            return Outcome::Done;
1159        };
1160        let entry = &self.entries[idx as usize];
1161        req.route = Some(&entry.pattern);
1162        report.route = Some(RouteId(idx));
1163
1164        // Authorization.
1165        if let Access::Rule(rule) = &entry.access {
1166            if let Err(d) = self.gate.authorize(who, rule, &req) {
1167                self.deny(d, &req, reply);
1168                return Outcome::Done;
1169            }
1170        }
1171        if let Some(cp) = &self.shared.checkpoint {
1172            match cp.check(env, Stage::Authorized) {
1173                Verdict::Pass => {}
1174                Verdict::Deny { status } => {
1175                    self.refuse(
1176                        status,
1177                        "checkpoint.denied",
1178                        EventKind::Revoked,
1179                        req.route,
1180                        None,
1181                        at,
1182                        reply,
1183                    );
1184                    return Outcome::Done;
1185                }
1186                _ => {
1187                    self.refuse(
1188                        403,
1189                        "checkpoint.killed",
1190                        EventKind::Revoked,
1191                        req.route,
1192                        None,
1193                        at,
1194                        reply,
1195                    );
1196                    return Outcome::Close;
1197                }
1198            }
1199        }
1200
1201        // CSRF on unsafe methods.
1202        let binding = who.csrf_binding();
1203        let csrf_state = match (&self.shared.csrf, entry.csrf) {
1204            (Some(engine), true) => {
1205                let scheme = if env.client().scheme() == Scheme::Https {
1206                    "https://"
1207                } else {
1208                    "http://"
1209                };
1210                let own = format!("{scheme}{}", host_raw.to_ascii_lowercase());
1211                match csrf::check(engine, http, binding, &own, &entry.limits) {
1212                    Ok(s) => s,
1213                    Err(code) => {
1214                        self.refuse(403, code, EventKind::Denied, req.route, None, at, reply);
1215                        return Outcome::Done;
1216                    }
1217                }
1218            }
1219            // Built only with an engine when a route needs one.
1220            (None, true) => {
1221                self.refuse(
1222                    500,
1223                    "csrf.no_engine",
1224                    EventKind::Failed,
1225                    req.route,
1226                    None,
1227                    at,
1228                    reply,
1229                );
1230                return Outcome::Done;
1231            }
1232            (_, false) => csrf::State::Off,
1233        };
1234
1235        // The handler, with its hooks, on a buffer web-net holds.
1236        let mut out = SCRATCH.with(Cell::take).unwrap_or_default();
1237        out.reset();
1238        let outcome = self.handle(
1239            entry,
1240            req,
1241            who,
1242            caps,
1243            (csrf_state, binding),
1244            &mut out,
1245            reply,
1246            &mut report.cache,
1247        );
1248        if self.shared.wipe {
1249            out.wipe();
1250        }
1251        if out.body.capacity() <= KEEP_CAPACITY && out.values.capacity() <= KEEP_CAPACITY {
1252            SCRATCH.with(|s| s.set(Some(out)));
1253        }
1254        outcome
1255    }
1256
1257    /// Health paths and static files.
1258    fn early(
1259        &self,
1260        http: &HttpRequest<'_>,
1261        at: At<'_>,
1262        path: &str,
1263        reply: &mut HttpReply,
1264    ) -> Option<Outcome> {
1265        let method = http.method();
1266        let get_like = matches!(method, Method::Get | Method::Head);
1267        if let Some((_, h)) = self.health.iter().find(|(p, _)| p.as_ref() == path) {
1268            if !get_like {
1269                self.not_get(method, at, reply);
1270                return Some(Outcome::Done);
1271            }
1272            let (status, text) = match h {
1273                Health::Live => (200, "ok"),
1274                Health::Ready(f) if f() => (200, "ready"),
1275                Health::Ready(_) => (503, "not ready"),
1276            };
1277            reply.text(status, text).header("cache-control", "no-store");
1278            self.secure(reply, at.tls, |_| false);
1279            return Some(Outcome::Done);
1280        }
1281        let (assets, rel) = self
1282            .assets
1283            .iter()
1284            .find_map(|a| a.strip(path).map(|rel| (a, rel)))?;
1285        if !get_like {
1286            self.not_get(method, at, reply);
1287            return Some(Outcome::Done);
1288        }
1289        let Some(found) = assets::find(assets, rel, http.header("accept-encoding")) else {
1290            self.refuse(
1291                404,
1292                "assets.none",
1293                EventKind::Refused,
1294                None,
1295                None,
1296                at,
1297                reply,
1298            );
1299            return Some(Outcome::Done);
1300        };
1301        let outcome = assets::serve(assets, &found, http, reply);
1302        self.secure(reply, at.tls, |_| false);
1303        Some(outcome)
1304    }
1305
1306    /// 405 (or 204 for `OPTIONS`) on a path that takes `GET` and `HEAD` only.
1307    fn not_get(&self, method: Method, at: At<'_>, reply: &mut HttpReply) {
1308        if method == Method::Options {
1309            reply.status(204).header("allow", "GET, HEAD, OPTIONS");
1310            self.secure(reply, at.tls, |_| false);
1311        } else {
1312            let (status, code) = if method == Method::Other {
1313                (501, "method.unknown")
1314            } else {
1315                (405, "method.not_allowed")
1316            };
1317            self.refuse(status, code, EventKind::Refused, None, None, at, reply);
1318            if status == 405 {
1319                reply.header("allow", "GET, HEAD, OPTIONS");
1320            }
1321        }
1322    }
1323
1324    #[allow(clippy::too_many_arguments)]
1325    fn handle(
1326        &self,
1327        entry: &Entry<G>,
1328        req: Request<'_>,
1329        who: &G::Identity,
1330        caps: Captures,
1331        (csrf_state, binding): (csrf::State, Option<&[u8]>),
1332        out: &mut Out,
1333        reply: &mut HttpReply,
1334        cacheable: &mut Cacheable,
1335    ) -> Outcome {
1336        let at = At {
1337            env: req.env,
1338            tls: req.tls,
1339            json: req.json,
1340        };
1341        let route = req.route;
1342        let http = req.http;
1343        let guest = who.is_guest();
1344        let (result, unverified) = {
1345            let mut cx = Cx {
1346                req,
1347                who,
1348                caps,
1349                names: &entry.names,
1350                out: &mut *out,
1351                shared: &self.shared,
1352                template: entry.template,
1353                csrf: csrf_state,
1354                binding,
1355                limits: &entry.limits,
1356                before_render: &entry.before_render,
1357                rendering: false,
1358            };
1359            let result = catch_unwind(AssertUnwindSafe(|| -> Result {
1360                for h in entry.routed.iter() {
1361                    h(&mut cx)?;
1362                }
1363                entry.handler.call(&mut cx)?;
1364                for h in entry.finish.iter() {
1365                    h(&mut cx)?;
1366                }
1367                Ok(())
1368            }));
1369            (result, cx.csrf == csrf::State::Pending)
1370        };
1371        match result {
1372            Ok(Ok(())) if unverified => {
1373                // A multipart route whose handler never read its form: the
1374                // token was never checked, so nothing it wrote may leave.
1375                out.reset();
1376                self.refuse(
1377                    403,
1378                    "csrf.unverified",
1379                    EventKind::Failed,
1380                    route,
1381                    None,
1382                    at,
1383                    reply,
1384                );
1385                Outcome::Done
1386            }
1387            Ok(Ok(())) if !out.invalid => {
1388                if !out.has("cache-control") {
1389                    let (for_guest, for_member) = &entry.cache_values;
1390                    if let Some(v) = if guest { for_guest } else { for_member } {
1391                        out.header("cache-control", v.as_bytes());
1392                    }
1393                }
1394                if let Some(c) = self.shared.compression {
1395                    compress(c, http.header("accept-encoding"), out);
1396                }
1397                *cacheable = match entry.cache {
1398                    CachePolicy::Guests { max_age } | CachePolicy::Public { max_age }
1399                        if guest
1400                            && matches!(http.method(), Method::Get | Method::Head)
1401                            && out.status == 200
1402                            && !out.has("set-cookie")
1403                            && out
1404                                .get("vary")
1405                                .is_none_or(|v| v.eq_ignore_ascii_case(b"accept-encoding")) =>
1406                    {
1407                        Cacheable::Guests { max_age }
1408                    }
1409                    _ => Cacheable::No,
1410                };
1411                reply.status(out.status);
1412                for &(ns, ne, vs, ve) in &out.spans {
1413                    let name = core::str::from_utf8(&out.names[ns as usize..ne as usize])
1414                        .unwrap_or("x-invalid");
1415                    reply.header(name, &out.values[vs as usize..ve as usize]);
1416                }
1417                if !out.has("x-content-type-options") {
1418                    reply.header("x-content-type-options", "nosniff");
1419                }
1420                if let Some(cors) = &entry.cors {
1421                    cors.actual(http.header("origin"), reply);
1422                }
1423                self.secure(reply, at.tls, |n| out.has(n));
1424                reply.body(&out.body);
1425                Outcome::Done
1426            }
1427            Ok(Ok(())) => {
1428                out.reset();
1429                self.refuse(
1430                    500,
1431                    "reply.invalid_field",
1432                    EventKind::Failed,
1433                    route,
1434                    None,
1435                    at,
1436                    reply,
1437                );
1438                Outcome::Done
1439            }
1440            Ok(Err(e)) => {
1441                out.reset();
1442                let src = std::error::Error::source(&e);
1443                self.refuse(
1444                    e.status(),
1445                    e.code(),
1446                    EventKind::Failed,
1447                    route,
1448                    src,
1449                    at,
1450                    reply,
1451                );
1452                Outcome::Done
1453            }
1454            Err(_) => {
1455                out.reset();
1456                self.refuse(
1457                    500,
1458                    "handler.panic",
1459                    EventKind::Panicked,
1460                    route,
1461                    None,
1462                    at,
1463                    reply,
1464                );
1465                Outcome::Done
1466            }
1467        }
1468    }
1469
1470    /// A gate's denial as a response.
1471    fn deny(&self, d: Denial, req: &Request<'_>, reply: &mut HttpReply) {
1472        let env = req.env;
1473        let at = At {
1474            env,
1475            tls: req.tls,
1476            json: req.json,
1477        };
1478        if d.kind() == DenialKind::SignIn {
1479            if let Some(page) = &self.shared.sign_in {
1480                if matches!(req.method(), Method::Get | Method::Head) {
1481                    let mut loc = String::with_capacity(64);
1482                    loc.push_str(&self.shared.base);
1483                    loc.push_str(page);
1484                    loc.push_str(if page.contains('?') {
1485                        "&next="
1486                    } else {
1487                        "?next="
1488                    });
1489                    let mut next = String::new();
1490                    next.push_str(&self.shared.base);
1491                    next.push_str(req.path);
1492                    if let Some(q) = req.query() {
1493                        next.push('?');
1494                        next.push_str(q);
1495                    }
1496                    encode_component(&next, &mut loc);
1497                    self.event(EventKind::Denied, env, req.route, 303, d.code(), None);
1498                    reply.status(303).header("location", loc.as_bytes());
1499                    self.secure(reply, at.tls, |_| false);
1500                    return;
1501                }
1502            }
1503        }
1504        let status = d.status_code();
1505        self.refuse(
1506            status,
1507            d.code(),
1508            EventKind::Denied,
1509            req.route,
1510            None,
1511            at,
1512            reply,
1513        );
1514        if status == 401 {
1515            reply.header("www-authenticate", self.shared.challenge.as_bytes());
1516        }
1517    }
1518
1519    fn event(
1520        &self,
1521        kind: EventKind,
1522        env: &Envelope,
1523        route: Option<&str>,
1524        status: u16,
1525        code: &'static str,
1526        source: Option<&(dyn std::error::Error + 'static)>,
1527    ) {
1528        self.shared.events.event(&Event {
1529            kind,
1530            id: RequestId::of(env),
1531            route,
1532            status,
1533            code,
1534            source,
1535        });
1536    }
1537
1538    /// The one function every refusal and error goes through: the status,
1539    /// the error template for it or the built-in page, the reference; the
1540    /// code and source to the events only.
1541    #[allow(clippy::too_many_arguments)]
1542    fn refuse(
1543        &self,
1544        status: u16,
1545        code: &'static str,
1546        kind: EventKind,
1547        route: Option<&str>,
1548        source: Option<&(dyn std::error::Error + 'static)>,
1549        at: At<'_>,
1550        reply: &mut HttpReply,
1551    ) {
1552        self.event(kind, at.env, route, status, code, source);
1553        let reference = RequestId::of(at.env).to_string();
1554        let mut body = Vec::with_capacity(512);
1555        if at.json {
1556            error::json_body(status, &reference, &mut body);
1557            reply
1558                .status(status)
1559                .header("content-type", "application/json")
1560                .header("x-content-type-options", "nosniff")
1561                .header("cache-control", "no-store")
1562                .body(&body);
1563            self.secure(reply, at.tls, |_| false);
1564            return;
1565        }
1566        let mut done = false;
1567        if let (Some(engine), Some(&(_, t))) = (
1568            self.shared.render.as_deref(),
1569            self.shared.errors.iter().find(|(s, _)| *s == status),
1570        ) {
1571            let fields = [
1572                ("status", Value::Int(i64::from(status))),
1573                ("title", Value::Text(error::title(status))),
1574                ("message", Value::Text(error::message(status))),
1575                ("reference", Value::Text(&reference)),
1576            ];
1577            let view = Fields(&fields);
1578            let mut out = Output::new(&mut body, self.shared.page_limit);
1579            if engine.render(t, &view, &mut out).is_ok() {
1580                done = true;
1581            } else {
1582                body.clear();
1583            }
1584        }
1585        if !done {
1586            error::builtin_page(status, &reference, &mut body);
1587        }
1588        reply
1589            .status(status)
1590            .header("content-type", "text/html; charset=utf-8")
1591            .header("x-content-type-options", "nosniff")
1592            .header("cache-control", "no-store")
1593            .body(&body);
1594        self.secure(reply, at.tls, |_| false);
1595    }
1596
1597    /// Adds the policy's security headers, except those `has` says the reply
1598    /// set already.
1599    fn secure(&self, reply: &mut HttpReply, tls: bool, has: impl Fn(&str) -> bool) {
1600        for (name, value) in self.shared.security.fields(tls) {
1601            if !has(name) {
1602                reply.header(name, value.as_bytes());
1603            }
1604        }
1605    }
1606
1607    /// How well this application claims a request, for [`Sites`](crate::Sites):
1608    /// the host's rank (2 listed or an alias, 1 a development host, 0 any
1609    /// host accepted) and the base path's length. `None`: not this site.
1610    pub(crate) fn claim(&self, authority: Option<&str>, path: &str) -> Option<(u8, usize)> {
1611        let (full, host_only) = host_parts(authority.unwrap_or(""));
1612        let listed = |list: &[Box<str>]| host_listed(list, full, host_only);
1613        let rank = if self.site.hosts.is_empty() {
1614            0
1615        } else if listed(&self.site.hosts) || listed(&self.site.aliases) {
1616            2
1617        } else if self.site.development && is_dev_host(host_only) {
1618            1
1619        } else {
1620            return None;
1621        };
1622        let base = self.shared.base.as_ref();
1623        let inside = base.is_empty()
1624            || path == base
1625            || path.strip_prefix(base).is_some_and(|r| r.starts_with('/'));
1626        inside.then_some((rank, base.len()))
1627    }
1628
1629    /// Whether HTTPS-only headers apply: the request came over HTTPS and is
1630    /// not for a development host.
1631    fn tls(&self, env: &Envelope, authority: Option<&str>) -> bool {
1632        if env.client().scheme() != Scheme::Https {
1633            return false;
1634        }
1635        let (_, host) = host_parts(authority.unwrap_or(""));
1636        !(self.site.development && is_dev_host(host))
1637    }
1638}
1639
1640impl<G: Gate> HttpService for App<G> {
1641    fn call(&self, request: &HttpRequest<'_>, env: &Envelope, reply: &mut HttpReply) -> Outcome {
1642        self.respond(request, env, reply)
1643    }
1644}
1645
1646impl<G: Gate> fmt::Debug for App<G> {
1647    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1648        f.debug_struct("App")
1649            .field("site", &self.site)
1650            .field("base", &self.shared.base)
1651            .field("routes", &self.entries.len())
1652            .finish_non_exhaustive()
1653    }
1654}
1655
1656/// Whether the client prefers JSON to HTML (no `Accept`: HTML).
1657fn wants_json(accept: Option<&[u8]>) -> bool {
1658    // Most requests never name JSON: skip the parse for them.
1659    accept.is_some_and(|a| a.windows(4).any(|w| w.eq_ignore_ascii_case(b"json")))
1660        && parse_net::best_media(accept, &["text/html", "application/json"]) == Some(1)
1661}
1662
1663/// gzip for a reply worth compressing that the client accepts. Adds `Vary`
1664/// for every compressible reply, compressed or not, so caches keep the two
1665/// apart.
1666fn compress(c: Compression, accept_encoding: Option<&[u8]>, out: &mut Out) {
1667    if out.status != 200 || out.has("content-encoding") {
1668        return;
1669    }
1670    if !out.get("content-type").is_some_and(compressible) {
1671        return;
1672    }
1673    if !out.has("vary") {
1674        out.header("vary", b"accept-encoding");
1675    }
1676    let accepts = accept_encoding
1677        .and_then(|v| parse_net::best_encoding(Some(v), &["gzip", "identity"]))
1678        == Some(0);
1679    if !accepts || out.body.len() < c.min_bytes {
1680        return;
1681    }
1682    let level = compress_lib::Level::new(c.level).unwrap_or(compress_lib::Level::DEFAULT);
1683    let packed = compress_lib::gzip::compress_with(&out.body, level);
1684    if packed.len() < out.body.len() {
1685        out.body.clear();
1686        out.body.extend_from_slice(&packed);
1687        out.header("content-encoding", b"gzip");
1688    }
1689}
1690
1691/// Percent-encodes a query component: everything but unreserved and `/`.
1692fn encode_component(s: &str, out: &mut String) {
1693    const HEX: &[u8; 16] = b"0123456789ABCDEF";
1694    for b in s.bytes() {
1695        if b.is_ascii_alphanumeric() || matches!(b, b'-' | b'.' | b'_' | b'~' | b'/') {
1696            out.push(b as char);
1697        } else {
1698            out.push('%');
1699            out.push(HEX[(b >> 4) as usize] as char);
1700            out.push(HEX[(b & 15) as usize] as char);
1701        }
1702    }
1703}
1704
1705#[cfg(test)]
1706mod tests {
1707    use super::*;
1708
1709    #[test]
1710    fn test_split_port() {
1711        assert_eq!(split_port("a.test:80"), ("a.test", Some("80")));
1712        assert_eq!(split_port("a.test"), ("a.test", None));
1713        assert_eq!(split_port("[::1]:8080"), ("[::1]", Some("8080")));
1714        assert_eq!(split_port("[::1]"), ("[::1]", None));
1715    }
1716
1717    #[test]
1718    fn test_valid_host() {
1719        for ok in ["example.com", "localhost:8080", "[::1]", "127.0.0.1:80"] {
1720            assert!(valid_host(ok), "{ok}");
1721        }
1722        for bad in ["", "a b", "a:", "a:x", "[zz]", "a/b"] {
1723            assert!(!valid_host(bad), "{bad}");
1724        }
1725    }
1726
1727    #[test]
1728    fn test_selector() {
1729        assert!(Routes::prefix("/blog").matches("/blog", None));
1730        assert!(Routes::prefix("/blog/").matches("/blog/:slug", None));
1731        assert!(!Routes::prefix("/blog").matches("/blogger", None));
1732        assert!(Routes::named(&["a"]).matches("/x", Some("a")));
1733        assert!(!Routes::named(&["a"]).matches("/x", None));
1734    }
1735}