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