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::cx::{Out, is_local_target};
18use crate::error::{self, BuildError, Problem};
19use crate::headers::SecurityHeaders;
20use crate::route::{Access, BoxHandler, Group, Route};
21use crate::router::{self, Captures, Trie};
22use crate::url::{self, TrailingSlash};
23use crate::{
24    Cx, Denial, DenialKind, Event, EventKind, Events, Fields, Gate, NoEvents, Open, Output, Render,
25    Request, RequestId, Result, Stage, TemplateId, Value,
26};
27
28/// The default budget for one rendered page.
29pub const DEFAULT_PAGE_LIMIT: usize = 8 << 20;
30
31/// Statuses with an error template lookup (`error/404`, ...).
32const ERROR_STATUSES: [u16; 14] = [
33    400, 401, 403, 404, 405, 409, 410, 413, 421, 422, 429, 500, 501, 503,
34];
35
36/// A named point in the pipeline where registered code runs, only for the
37/// routes it registered for. A route with no hooks pays nothing.
38#[non_exhaustive]
39#[derive(Clone, Copy, Debug, PartialEq, Eq)]
40pub enum Hook {
41    /// After the access check passed, before the handler: locale, feature
42    /// flags, an A/B variant. An error ends the request.
43    Routed,
44    /// After the handler succeeded, before the reply leaves: add headers.
45    /// An error replaces the reply with the error page.
46    Finish,
47}
48
49/// Which routes a hook runs for.
50#[derive(Clone, Debug)]
51pub struct Routes(Selector);
52
53#[derive(Clone, Debug)]
54enum Selector {
55    All,
56    Prefix(String),
57    Named(Vec<&'static str>),
58}
59
60impl Routes {
61    /// Every route.
62    #[must_use]
63    pub fn all() -> Self {
64        Self(Selector::All)
65    }
66
67    /// Routes whose pattern starts with `prefix` (`/blog`).
68    #[must_use]
69    pub fn prefix(prefix: impl Into<String>) -> Self {
70        Self(Selector::Prefix(prefix.into()))
71    }
72
73    /// Routes with one of these names.
74    #[must_use]
75    pub fn named(names: &[&'static str]) -> Self {
76        Self(Selector::Named(names.to_vec()))
77    }
78
79    fn matches(&self, pattern: &str, name: Option<&str>) -> bool {
80        match &self.0 {
81            Selector::All => true,
82            Selector::Prefix(p) => {
83                let p = p.trim_end_matches('/');
84                pattern == p
85                    || pattern
86                        .strip_prefix(p)
87                        .is_some_and(|rest| rest.starts_with('/'))
88            }
89            Selector::Named(names) => name.is_some_and(|n| names.contains(&n)),
90        }
91    }
92}
93
94type HookFn<I> = Arc<dyn Fn(&mut Cx<'_, I>) -> Result + Send + Sync>;
95
96/// What [`App::classify`] tells a dispatcher before admission.
97#[derive(Clone, Copy, Debug, PartialEq, Eq)]
98pub struct Classified {
99    /// The route that will run, if one matched.
100    pub route: Option<RouteId>,
101    /// How its handler behaves (`Quick` when nothing matched).
102    pub work: WorkClass,
103}
104
105/// A declared route (one method of one pattern), numbered in declaration
106/// order.
107#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
108pub struct RouteId(pub u32);
109
110/// The parts of the application every handler can reach.
111pub(crate) struct Shared {
112    pub(crate) render: Option<Box<dyn Render>>,
113    pub(crate) checkpoint: Option<Box<dyn crate::Checkpoint>>,
114    pub(crate) events: Box<dyn Events>,
115    pub(crate) page_limit: usize,
116    pub(crate) base: Box<str>,
117    errors: Vec<(u16, TemplateId)>,
118    sign_in: Option<Box<str>>,
119    challenge: Box<str>,
120    wipe: bool,
121    security: SecurityHeaders,
122}
123
124/// Where a reply is going: the request's envelope, and whether security
125/// headers for HTTPS apply (HTTPS, and not a development host).
126#[derive(Clone, Copy)]
127pub(crate) struct At<'a> {
128    pub(crate) env: &'a Envelope,
129    pub(crate) tls: bool,
130}
131
132type ReadyFn = Arc<dyn Fn() -> bool + Send + Sync>;
133
134#[derive(Clone)]
135enum Health {
136    Live,
137    Ready(ReadyFn),
138}
139
140#[derive(Clone, Debug, Default)]
141struct Site {
142    /// Lower case; the first is canonical.
143    hosts: Vec<Box<str>>,
144    aliases: Vec<Box<str>>,
145    development: bool,
146    slash: TrailingSlash,
147    lowercase: bool,
148    https_only: bool,
149}
150
151struct Entry<G: Gate> {
152    handler: BoxHandler<G::Identity>,
153    access: Access<G::Rule>,
154    names: Box<[Box<str>]>,
155    pattern: Box<str>,
156    work: WorkClass,
157    template: Option<TemplateId>,
158    routed: Box<[HookFn<G::Identity>]>,
159    finish: Box<[HookFn<G::Identity>]>,
160}
161
162struct Endpoint {
163    /// Entry per method, by `Method::index`.
164    methods: [Option<u32>; 10],
165    allow: Box<str>,
166}
167
168/// A web application: one site, its routes, and its gate. Built once at
169/// start-up with [`App::builder`] or [`App::with_gate`]; immutable after.
170///
171/// It is a `serve_net::HttpService`, so it runs on serve-net as it is, and
172/// it has [`respond`](Self::respond) and
173/// [`respond_identified`](Self::respond_identified) for a wrapper that
174/// calls it from its own service.
175///
176/// ```
177/// use web_net::{App, get};
178///
179/// let app = App::builder()
180///     .route(get("/", |cx| { cx.html(200, "<h1>Hello</h1>"); Ok(()) }).public())
181///     .route(get("/users/:id", |cx| {
182///         let id: u64 = cx.param_as("id")?;
183///         cx.text(200, &format!("user {id}"));
184///         Ok(())
185///     }).public())
186///     .build()?;
187///
188/// let reply = serve_net::testing::respond(&app, b"GET /users/7 HTTP/1.1\r\nHost: a\r\n\r\n");
189/// assert_eq!((reply.status, reply.text()), (200, "user 7"));
190///
191/// let reply = serve_net::testing::respond(&app, b"GET /users/7/ HTTP/1.1\r\nHost: a\r\n\r\n");
192/// assert_eq!(reply.status, 301);
193/// assert_eq!(reply.header("location"), Some(&b"/users/7"[..]));
194/// # Ok::<(), web_net::BuildError>(())
195/// ```
196pub struct App<G: Gate = Open> {
197    gate: G,
198    shared: Shared,
199    site: Site,
200    trie: Trie,
201    endpoints: Vec<Endpoint>,
202    entries: Vec<Entry<G>>,
203    allow_all: Box<str>,
204    assets: Vec<Assets>,
205    health: Vec<(Box<str>, Health)>,
206}
207
208impl App<Open> {
209    /// A builder with the [`Open`] gate: every visitor is a guest and every
210    /// route is declared public.
211    #[must_use]
212    pub fn builder() -> Builder<Open> {
213        Builder::new(Open)
214    }
215}
216
217impl<G: Gate> App<G> {
218    /// A builder with `gate` for authentication and authorization.
219    #[must_use]
220    pub fn with_gate(gate: G) -> Builder<G> {
221        Builder::new(gate)
222    }
223}
224
225/// Declares an application. Every mistake is reported by
226/// [`build`](Self::build) at once, before anything listens.
227pub struct Builder<G: Gate> {
228    gate: G,
229    routes: Vec<Route<G::Identity, G::Rule>>,
230    hooks: Vec<(Hook, Routes, HookFn<G::Identity>)>,
231    site: Site,
232    base: String,
233    render: Option<Box<dyn Render>>,
234    checkpoint: Option<Box<dyn crate::Checkpoint>>,
235    events: Box<dyn Events>,
236    page_limit: usize,
237    sign_in: Option<String>,
238    challenge: String,
239    wipe: bool,
240    security: SecurityHeaders,
241    assets: Vec<Assets>,
242    health: Vec<(String, Health)>,
243}
244
245impl<G: Gate> Builder<G> {
246    fn new(gate: G) -> Self {
247        Self {
248            gate,
249            routes: Vec::new(),
250            hooks: Vec::new(),
251            site: Site::default(),
252            base: String::new(),
253            render: None,
254            checkpoint: None,
255            events: Box::new(NoEvents),
256            page_limit: DEFAULT_PAGE_LIMIT,
257            sign_in: None,
258            challenge: "Session".into(),
259            wipe: false,
260            security: SecurityHeaders::default(),
261            assets: Vec::new(),
262            health: Vec::new(),
263        }
264    }
265
266    /// Serves a folder of files under a mount path, before routing and
267    /// without the gate (files are public).
268    #[must_use]
269    pub fn assets(mut self, assets: Assets) -> Self {
270        self.assets.push(assets);
271        self
272    }
273
274    /// A liveness path (`/healthz`): `200 ok` whenever the process answers.
275    /// Answered before routing and without the gate.
276    #[must_use]
277    pub fn health(mut self, path: &str) -> Self {
278        self.health.push((path.to_owned(), Health::Live));
279        self
280    }
281
282    /// A readiness path (`/readyz`): `200 ready` when `check` says so,
283    /// `503 not ready` otherwise. Keep `check` to a flag read: it runs on the
284    /// request path.
285    #[must_use]
286    pub fn ready(mut self, path: &str, check: impl Fn() -> bool + Send + Sync + 'static) -> Self {
287        self.health
288            .push((path.to_owned(), Health::Ready(Arc::new(check))));
289        self
290    }
291
292    /// The security headers every reply carries (default
293    /// [`SecurityHeaders::default`]).
294    #[must_use]
295    pub fn security_headers(mut self, policy: SecurityHeaders) -> Self {
296        self.security = policy;
297        self
298    }
299
300    /// Adds a route.
301    #[must_use]
302    pub fn route(mut self, route: Route<G::Identity, G::Rule>) -> Self {
303        self.routes.push(route);
304        self
305    }
306
307    /// Adds a group's routes.
308    #[must_use]
309    pub fn group(mut self, group: Group<G::Identity, G::Rule>) -> Self {
310        self.routes.extend(group.into_routes());
311        self
312    }
313
314    /// Runs `f` at `hook` for the routes `routes` selects.
315    #[must_use]
316    pub fn hook<F>(mut self, hook: Hook, routes: Routes, f: F) -> Self
317    where
318        F: Fn(&mut Cx<'_, G::Identity>) -> Result + Send + Sync + 'static,
319    {
320        self.hooks.push((hook, routes, Arc::new(f)));
321        self
322    }
323
324    /// A host the site answers (`example.com`, or `localhost:8080` with its
325    /// port). The first one is canonical. With no host declared the site
326    /// answers any host; with hosts declared, any other host gets 421.
327    #[must_use]
328    pub fn host(mut self, host: &str) -> Self {
329        self.site.hosts.push(host.to_ascii_lowercase().into());
330        self
331    }
332
333    /// A host that is redirected (301, or 308 for unsafe methods) to the
334    /// canonical host (`www.example.com` to `example.com`).
335    #[must_use]
336    pub fn alias(mut self, host: &str) -> Self {
337        self.site.aliases.push(host.to_ascii_lowercase().into());
338        self
339    }
340
341    /// A development instance: `localhost`, `127.0.0.1`, `[::1]`, and
342    /// `*.localhost` are answered as the site (with any port), and HTTPS is
343    /// not required for them. Never set this in production: it is the only
344    /// way a `Host: localhost` is trusted.
345    #[must_use]
346    pub fn development(mut self) -> Self {
347        self.site.development = true;
348        self
349    }
350
351    /// The path the site is installed under (`/blog`). It is removed before
352    /// routing and added back by [`Cx::url`], so routes and links are written
353    /// as if the site were at `/`. Requests outside it get 404.
354    #[must_use]
355    pub fn base(mut self, base: &str) -> Self {
356        self.base = base.to_owned();
357        self
358    }
359
360    /// The trailing-slash policy (default [`TrailingSlash::Strip`]).
361    #[must_use]
362    pub fn trailing_slash(mut self, policy: TrailingSlash) -> Self {
363        self.site.slash = policy;
364        self
365    }
366
367    /// Paths are lower case: one with an upper-case letter is redirected.
368    #[must_use]
369    pub fn lowercase_paths(mut self) -> Self {
370        self.site.lowercase = true;
371        self
372    }
373
374    /// Plain HTTP is redirected to HTTPS on the canonical host (behind a
375    /// TLS proxy, serve-net must trust the proxy's scheme). Needs a host.
376    #[must_use]
377    pub fn https_only(mut self) -> Self {
378        self.site.https_only = true;
379        self
380    }
381
382    /// The site-relative sign-in page. A [`Denial::sign_in`] on a `GET` or
383    /// `HEAD` is then answered with a 303 to it, the page asked for passed
384    /// as `next`; without it, or for other methods, the answer is 401.
385    #[must_use]
386    pub fn sign_in(mut self, path: &str) -> Self {
387        self.sign_in = Some(path.to_owned());
388        self
389    }
390
391    /// The `WWW-Authenticate` challenge sent with a 401 (default
392    /// `Session`).
393    #[must_use]
394    pub fn challenge(mut self, challenge: &str) -> Self {
395        challenge.clone_into(&mut self.challenge);
396        self
397    }
398
399    /// The template engine.
400    #[must_use]
401    pub fn render(mut self, engine: impl Render) -> Self {
402        self.render = Some(Box::new(engine));
403        self
404    }
405
406    /// Where refusals, denials, and failures are reported.
407    #[must_use]
408    pub fn events(mut self, events: impl Events) -> Self {
409        self.events = Box::new(events);
410        self
411    }
412
413    /// The in-flight revocation check at web-net's stages.
414    #[must_use]
415    pub fn checkpoint(mut self, checkpoint: impl crate::Checkpoint) -> Self {
416        self.checkpoint = Some(Box::new(checkpoint));
417        self
418    }
419
420    /// The budget for one rendered page (default 8 MiB).
421    #[must_use]
422    pub fn page_limit(mut self, bytes: usize) -> Self {
423        self.page_limit = bytes;
424        self
425    }
426
427    /// Zero the reply buffers web-net reuses after every request, so one
428    /// visitor's page never lingers in memory another request reuses. Costs
429    /// one pass over the bytes written.
430    #[must_use]
431    pub fn wipe_buffers(mut self) -> Self {
432        self.wipe = true;
433        self
434    }
435
436    /// Compiles the application.
437    ///
438    /// # Errors
439    ///
440    /// A [`BuildError`] listing every problem: a route without declared
441    /// access, a malformed pattern, two handlers for one method of one
442    /// path, an unknown template, a malformed host or base path.
443    pub fn build(self) -> std::result::Result<App<G>, BuildError> {
444        let mut problems = Vec::new();
445        let mut problem = |subject: &str, message: String| {
446            problems.push(Problem {
447                subject: subject.to_owned(),
448                message,
449            });
450        };
451
452        // Site.
453        let base = self.base.trim_end_matches('/');
454        if !base.is_empty() {
455            match url::normalize(base, self.site.lowercase) {
456                Ok(n) if n == base && !base.contains(['?', '#']) => {}
457                _ => problem(
458                    "base",
459                    format!("'{}' is not a canonical path such as '/blog'", self.base),
460                ),
461            }
462        }
463        for h in self.site.hosts.iter().chain(&self.site.aliases) {
464            if !valid_host(h) {
465                problem("host", format!("'{h}' is not a host name or address"));
466            }
467        }
468        if self.site.https_only && self.site.hosts.is_empty() {
469            problem(
470                "https_only",
471                "redirecting to HTTPS needs a declared host; add .host(\"example.com\")".into(),
472            );
473        }
474        if let Some(s) = &self.sign_in {
475            if !is_local_target(s) {
476                problem("sign_in", format!("'{s}' is not a site-relative path"));
477            }
478        }
479        if self.challenge.is_empty() || !self.challenge.bytes().all(|b| (0x20..0x7f).contains(&b)) {
480            problem("challenge", "the challenge must be visible ASCII".into());
481        }
482
483        if !self.security.valid() {
484            problem(
485                "security_headers",
486                "every header value must be visible ASCII and not empty".into(),
487            );
488        }
489        let mut assets = self.assets;
490        for a in &mut assets {
491            let subject = format!("assets {}", a.mount);
492            if a.mount.is_empty()
493                || !is_local_target(&a.mount)
494                || url::normalize(&a.mount, self.site.lowercase).map_or(true, |n| n != a.mount)
495            {
496                problem(
497                    &subject,
498                    "the mount must be a canonical path such as '/static'".into(),
499                );
500            }
501            match std::fs::canonicalize(&a.dir) {
502                Ok(root) if root.is_dir() => a.root = root,
503                _ => problem(
504                    &subject,
505                    format!("'{}' is not a folder that exists", a.dir.display()),
506                ),
507            }
508            for r in &self.routes {
509                if a.strip(&r.pattern).is_some() {
510                    problem(
511                        &r.pattern,
512                        format!(
513                            "the route is under the assets mount '{}' and can never run",
514                            a.mount
515                        ),
516                    );
517                }
518            }
519        }
520        let mut health = Vec::new();
521        for (p, h) in self.health {
522            if !is_local_target(&p) || url::normalize(&p, false).map_or(true, |n| n != p) {
523                problem(
524                    "health",
525                    format!("'{p}' is not a canonical site-relative path"),
526                );
527            }
528            health.push((p.into_boxed_str(), h));
529        }
530
531        // Error templates.
532        let mut errors = Vec::new();
533        if let Some(r) = &self.render {
534            for s in ERROR_STATUSES {
535                if let Some(t) = r.template(&format!("error/{s}")) {
536                    errors.push((s, t));
537                }
538            }
539        }
540
541        // Routes.
542        let mut trie = Trie::default();
543        let mut endpoints: Vec<Endpoint> = Vec::new();
544        let mut entries: Vec<Entry<G>> = Vec::new();
545        let mut all = MethodSet::EMPTY;
546        for r in self.routes {
547            let parts = match router::parse(&r.pattern) {
548                Ok(p) => p,
549                Err(e) => {
550                    problem(&r.pattern, e.message());
551                    continue;
552                }
553            };
554            if r.handlers.is_empty() {
555                problem(&r.pattern, "the route has no handler".into());
556                continue;
557            }
558            if matches!(r.access, Access::Undeclared) {
559                problem(
560                    &r.pattern,
561                    "the route declares no access; add .public() or .allow(rule)".into(),
562                );
563                continue;
564            }
565            let template = match (r.template, &self.render) {
566                (None, _) => None,
567                (Some(t), None) => {
568                    problem(
569                        &r.pattern,
570                        format!("template '{t}' is declared but no render engine is set"),
571                    );
572                    None
573                }
574                (Some(t), Some(engine)) => {
575                    let id = engine.template(t);
576                    if id.is_none() {
577                        problem(&r.pattern, format!("the engine has no template '{t}'"));
578                    }
579                    id
580                }
581            };
582            let ep = trie.insert(&parts, endpoints.len() as u32) as usize;
583            if ep == endpoints.len() {
584                endpoints.push(Endpoint {
585                    methods: [None; 10],
586                    allow: Box::from(""),
587                });
588            }
589            let names: Box<[Box<str>]> = router::names(&parts).into();
590            for (m, h) in r.handlers {
591                let subject = format!("{} {}", m.as_str().unwrap_or("?"), r.pattern);
592                if m == Method::Other || m == Method::Connect || m == Method::Trace {
593                    problem(&subject, "this method cannot be routed".into());
594                    continue;
595                }
596                let slot = &mut endpoints[ep].methods[m.index() as usize];
597                if slot.is_some() {
598                    problem(
599                        &subject,
600                        "a handler for this method and path exists already".into(),
601                    );
602                    continue;
603                }
604                *slot = Some(entries.len() as u32);
605                let pick = |at: Hook| -> Box<[HookFn<G::Identity>]> {
606                    self.hooks
607                        .iter()
608                        .filter(|(h, sel, _)| *h == at && sel.matches(&r.pattern, r.name))
609                        .map(|(_, _, f)| Arc::clone(f))
610                        .collect()
611                };
612                entries.push(Entry {
613                    handler: h,
614                    access: r.access.clone(),
615                    names: names.clone(),
616                    pattern: r.pattern.as_str().into(),
617                    work: r.work,
618                    template,
619                    routed: pick(Hook::Routed),
620                    finish: pick(Hook::Finish),
621                });
622            }
623        }
624        for e in &mut endpoints {
625            let mut set = MethodSet::EMPTY;
626            for m in [
627                Method::Get,
628                Method::Head,
629                Method::Post,
630                Method::Put,
631                Method::Delete,
632                Method::Options,
633                Method::Patch,
634            ] {
635                if e.methods[m.index() as usize].is_some() {
636                    set = set.with(m);
637                }
638            }
639            if set.contains(Method::Get) {
640                set = set.with(Method::Head);
641            }
642            set = set.with(Method::Options);
643            all = all.union(set);
644            let mut v = Vec::new();
645            set.write_allow(&mut v);
646            e.allow = String::from_utf8(v).unwrap_or_default().into();
647        }
648        if !problems.is_empty() {
649            return Err(BuildError::new(problems));
650        }
651        let mut v = Vec::new();
652        all.with(Method::Options).write_allow(&mut v);
653        Ok(App {
654            gate: self.gate,
655            shared: Shared {
656                render: self.render,
657                checkpoint: self.checkpoint,
658                events: self.events,
659                page_limit: self.page_limit,
660                base: base.into(),
661                errors,
662                sign_in: self.sign_in.map(Into::into),
663                challenge: self.challenge.into(),
664                wipe: self.wipe,
665                security: self.security,
666            },
667            site: self.site,
668            trie,
669            endpoints,
670            entries,
671            allow_all: String::from_utf8(v).unwrap_or_default().into(),
672            assets,
673            health,
674        })
675    }
676}
677
678impl<G: Gate> fmt::Debug for Builder<G> {
679    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
680        f.debug_struct("Builder")
681            .field("routes", &self.routes)
682            .field("site", &self.site)
683            .field("base", &self.base)
684            .finish_non_exhaustive()
685    }
686}
687
688fn valid_host(h: &str) -> bool {
689    let (host, port) = split_port(h);
690    let host_ok = if let Some(inner) = host.strip_prefix('[').and_then(|r| r.strip_suffix(']')) {
691        inner.parse::<std::net::Ipv6Addr>().is_ok()
692    } else {
693        !host.is_empty()
694            && host.len() <= 253
695            && host
696                .bytes()
697                .all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'.')
698    };
699    host_ok && port.is_none_or(|p| !p.is_empty() && p.parse::<u16>().is_ok())
700}
701
702/// `example.com:8080` to (`example.com`, `Some("8080")`); `[::1]:80` to
703/// (`[::1]`, `Some("80")`).
704fn split_port(a: &str) -> (&str, Option<&str>) {
705    if a.starts_with('[') {
706        return match a.find(']') {
707            Some(i) => match a[i + 1..].strip_prefix(':') {
708                Some(p) => (&a[..=i], Some(p)),
709                None => (a, None),
710            },
711            None => (a, None),
712        };
713    }
714    match a.rsplit_once(':') {
715        Some((h, p)) => (h, Some(p)),
716        None => (a, None),
717    }
718}
719
720fn is_dev_host(host: &str) -> bool {
721    matches!(host, "localhost" | "127.0.0.1" | "[::1]") || host.ends_with(".localhost")
722}
723
724thread_local! {
725    /// Reply buffers reused between requests on one thread. Taken for the
726    /// length of one request and put back, so nothing about a request lives
727    /// here between calls.
728    static SCRATCH: Cell<Option<Box<Out>>> = const { Cell::new(None) };
729}
730
731/// Buffers bigger than this are dropped instead of kept.
732const KEEP_CAPACITY: usize = 1 << 20;
733
734/// Where the site check sent a request.
735enum Placed<'a> {
736    Here(Cow<'a, str>),
737    Redirect(String),
738    Refuse(u16, &'static str, Option<url::UrlError>),
739}
740
741impl<G: Gate> App<G> {
742    /// Runs the whole pipeline for one request, authentication included.
743    pub fn respond(&self, req: &HttpRequest<'_>, env: &Envelope, reply: &mut HttpReply) -> Outcome {
744        self.run(req, env, None, reply)
745    }
746
747    /// Runs the pipeline for a request whose caller authenticated it
748    /// already: web-net takes `who` as given and does not call the gate's
749    /// `authenticate`. Authorization still runs for every route that is
750    /// not public.
751    pub fn respond_identified(
752        &self,
753        req: &HttpRequest<'_>,
754        env: &Envelope,
755        who: &G::Identity,
756        reply: &mut HttpReply,
757    ) -> Outcome {
758        self.run(req, env, Some(who), reply)
759    }
760
761    /// Looks at a head before admission: which route would run and how it
762    /// behaves. Nothing is answered and no gate is called.
763    #[must_use]
764    pub fn classify(&self, head: &HttpHead<'_>) -> Classified {
765        let none = Classified {
766            route: None,
767            work: WorkClass::Quick,
768        };
769        let Placed::Here(path) = self.place(head.authority(), head.path(), Scheme::Https, true)
770        else {
771            return none;
772        };
773        let Some((ep, _)) = self.trie.find(&path) else {
774            return none;
775        };
776        match self.entry_for(ep as usize, head.method()) {
777            Some(i) => Classified {
778                route: Some(RouteId(i)),
779                work: self.entries[i as usize].work,
780            },
781            None => none,
782        }
783    }
784
785    /// The gate.
786    #[must_use]
787    pub fn gate(&self) -> &G {
788        &self.gate
789    }
790
791    fn entry_for(&self, ep: usize, m: Method) -> Option<u32> {
792        let methods = &self.endpoints[ep].methods;
793        match m {
794            Method::Other => None,
795            Method::Head => {
796                methods[Method::Head.index() as usize].or(methods[Method::Get.index() as usize])
797            }
798            m => methods[m.index() as usize],
799        }
800    }
801
802    /// Steps 1 and 2: which site, which canonical path. `quiet` (for
803    /// `classify`) skips building a redirect.
804    fn place<'a>(
805        &self,
806        authority: Option<&'a str>,
807        raw: &'a str,
808        scheme: Scheme,
809        quiet: bool,
810    ) -> Placed<'a> {
811        let site = &self.site;
812        let authority = authority.unwrap_or("");
813        let lower = authority.to_ascii_lowercase();
814        let lower = lower.trim_end_matches('.');
815        let host_only = split_port(lower).0.trim_end_matches('.');
816        let listed = |list: &[Box<str>]| {
817            list.iter().any(|h| {
818                let h = h.as_ref();
819                h == lower || (split_port(h).1.is_none() && h == host_only)
820            })
821        };
822        let dev = site.development && is_dev_host(host_only);
823        let mut target_host: Option<&str> = None;
824        if !site.hosts.is_empty() && !dev && !authority.is_empty() && !listed(&site.hosts) {
825            if listed(&site.aliases) {
826                target_host = site.hosts.first().map(AsRef::as_ref);
827            } else {
828                return Placed::Refuse(421, "host.unknown", None);
829            }
830        }
831        let mut https = scheme == Scheme::Https;
832        if site.https_only && !https && !dev {
833            https = true;
834            target_host = target_host.or(site.hosts.first().map(AsRef::as_ref));
835        }
836
837        // The path: canonical, inside the base, with the slash policy.
838        let canon = match url::normalize(raw, site.lowercase) {
839            Ok(c) => c,
840            Err(e) => return Placed::Refuse(400, e.code(), Some(e)),
841        };
842        let base = self.shared.base.as_ref();
843        let c: &str = &canon;
844        let rel_start = if base.is_empty() {
845            0
846        } else if c == base {
847            c.len()
848        } else if c.strip_prefix(base).is_some_and(|r| r.starts_with('/')) {
849            base.len()
850        } else {
851            return Placed::Refuse(404, "site.outside_base", None);
852        };
853        let rel = &c[rel_start..];
854        let mut fixed: Option<String> = None;
855        if rel.is_empty() {
856            fixed = Some("/".into());
857        } else if rel.len() > 1 {
858            match site.slash {
859                TrailingSlash::Strip => {
860                    if let Some(s) = rel.strip_suffix('/') {
861                        fixed = Some(s.to_owned());
862                    }
863                }
864                TrailingSlash::Add => {
865                    let last = rel.rsplit('/').next().unwrap_or("");
866                    if !rel.ends_with('/') && !last.contains('.') {
867                        fixed = Some(format!("{rel}/"));
868                    }
869                }
870                TrailingSlash::Ignore => {}
871            }
872        }
873        if target_host.is_none() && fixed.is_none() {
874            if let Cow::Borrowed(c) = canon {
875                let mut path = &c[rel_start..];
876                if site.slash == TrailingSlash::Ignore && path.len() > 1 {
877                    path = path.strip_suffix('/').unwrap_or(path);
878                }
879                return Placed::Here(Cow::Borrowed(path));
880            }
881        }
882        if quiet {
883            return Placed::Refuse(0, "", None);
884        }
885        let rel_out = fixed.as_deref().unwrap_or(rel);
886        let mut loc = String::with_capacity(raw.len() + 32);
887        if let Some(h) = target_host {
888            loc.push_str(if https { "https://" } else { "http://" });
889            loc.push_str(h);
890        }
891        loc.push_str(base);
892        loc.push_str(rel_out);
893        Placed::Redirect(loc)
894    }
895
896    fn run(
897        &self,
898        http: &HttpRequest<'_>,
899        env: &Envelope,
900        who: Option<&G::Identity>,
901        reply: &mut HttpReply,
902    ) -> Outcome {
903        let method = http.method();
904        let raw_path = http.path();
905        let authority = http.head().authority();
906        let at = At {
907            env,
908            tls: self.tls(env, authority),
909        };
910        if raw_path == "*" {
911            if method == Method::Options {
912                reply.status(204).header("allow", self.allow_all.as_bytes());
913                self.secure(reply, at.tls, |_| false);
914            } else {
915                self.refuse(
916                    400,
917                    "url.asterisk",
918                    EventKind::Refused,
919                    None,
920                    None,
921                    at,
922                    reply,
923                );
924            }
925            return Outcome::Done;
926        }
927        let host_raw = authority.unwrap_or("");
928        let placed = self.place(authority, raw_path, env.client().scheme(), false);
929        let path = match placed {
930            Placed::Here(path) => path,
931            Placed::Redirect(mut loc) => {
932                if let Some(q) = http.query() {
933                    loc.push('?');
934                    loc.push_str(q);
935                }
936                let status = if matches!(method, Method::Get | Method::Head) {
937                    301
938                } else {
939                    308
940                };
941                reply.status(status).header("location", loc.as_bytes());
942                self.secure(reply, at.tls, |_| false);
943                return Outcome::Done;
944            }
945            Placed::Refuse(status, code, src) => {
946                let src = src
947                    .as_ref()
948                    .map(|e| e as &(dyn std::error::Error + 'static));
949                self.refuse(status, code, EventKind::Refused, None, src, at, reply);
950                return Outcome::Done;
951            }
952        };
953        // Early: health paths and files, before the gate and the router.
954        if let Some(outcome) = self.early(http, at, &path, reply) {
955            return outcome;
956        }
957
958        let mut req = Request {
959            http,
960            env,
961            path: &path,
962            host: host_raw,
963            route: None,
964            tls: at.tls,
965        };
966
967        // Authentication, unless the caller did it.
968        let owned;
969        let who: &G::Identity = match who {
970            Some(w) => w,
971            None => match self.gate.authenticate(&req) {
972                Ok(w) => {
973                    owned = w;
974                    &owned
975                }
976                Err(d) => {
977                    self.deny(d, &req, reply);
978                    return Outcome::Done;
979                }
980            },
981        };
982
983        // Route and method.
984        let Some((ep, caps)) = self.trie.find(&path) else {
985            self.refuse(404, "route.none", EventKind::Refused, None, None, at, reply);
986            return Outcome::Done;
987        };
988        let endpoint = &self.endpoints[ep as usize];
989        let Some(idx) = self.entry_for(ep as usize, method) else {
990            if method == Method::Options {
991                reply.status(204).header("allow", endpoint.allow.as_bytes());
992                self.secure(reply, at.tls, |_| false);
993                return Outcome::Done;
994            }
995            let status = if method == Method::Other { 501 } else { 405 };
996            let code = if status == 501 {
997                "method.unknown"
998            } else {
999                "method.not_allowed"
1000            };
1001            self.refuse(status, code, EventKind::Refused, None, None, at, reply);
1002            if status == 405 {
1003                reply.header("allow", endpoint.allow.as_bytes());
1004            }
1005            return Outcome::Done;
1006        };
1007        let entry = &self.entries[idx as usize];
1008        req.route = Some(&entry.pattern);
1009
1010        // Authorization.
1011        if let Access::Rule(rule) = &entry.access {
1012            if let Err(d) = self.gate.authorize(who, rule, &req) {
1013                self.deny(d, &req, reply);
1014                return Outcome::Done;
1015            }
1016        }
1017        if let Some(cp) = &self.shared.checkpoint {
1018            match cp.check(env, Stage::Authorized) {
1019                Verdict::Pass => {}
1020                Verdict::Deny { status } => {
1021                    self.refuse(
1022                        status,
1023                        "checkpoint.denied",
1024                        EventKind::Revoked,
1025                        req.route,
1026                        None,
1027                        at,
1028                        reply,
1029                    );
1030                    return Outcome::Done;
1031                }
1032                _ => {
1033                    self.refuse(
1034                        403,
1035                        "checkpoint.killed",
1036                        EventKind::Revoked,
1037                        req.route,
1038                        None,
1039                        at,
1040                        reply,
1041                    );
1042                    return Outcome::Close;
1043                }
1044            }
1045        }
1046
1047        // The handler, with its hooks, on a buffer web-net holds.
1048        let mut out = SCRATCH.with(Cell::take).unwrap_or_default();
1049        out.reset();
1050        let outcome = self.handle(entry, req, who, caps, &mut out, reply);
1051        if self.shared.wipe {
1052            out.wipe();
1053        }
1054        if out.body.capacity() <= KEEP_CAPACITY && out.values.capacity() <= KEEP_CAPACITY {
1055            SCRATCH.with(|s| s.set(Some(out)));
1056        }
1057        outcome
1058    }
1059
1060    /// Health paths and static files.
1061    fn early(
1062        &self,
1063        http: &HttpRequest<'_>,
1064        at: At<'_>,
1065        path: &str,
1066        reply: &mut HttpReply,
1067    ) -> Option<Outcome> {
1068        let method = http.method();
1069        let get_like = matches!(method, Method::Get | Method::Head);
1070        if let Some((_, h)) = self.health.iter().find(|(p, _)| p.as_ref() == path) {
1071            if !get_like {
1072                self.not_get(method, at, reply);
1073                return Some(Outcome::Done);
1074            }
1075            let (status, text) = match h {
1076                Health::Live => (200, "ok"),
1077                Health::Ready(f) if f() => (200, "ready"),
1078                Health::Ready(_) => (503, "not ready"),
1079            };
1080            reply.text(status, text).header("cache-control", "no-store");
1081            self.secure(reply, at.tls, |_| false);
1082            return Some(Outcome::Done);
1083        }
1084        let (assets, rel) = self
1085            .assets
1086            .iter()
1087            .find_map(|a| a.strip(path).map(|rel| (a, rel)))?;
1088        if !get_like {
1089            self.not_get(method, at, reply);
1090            return Some(Outcome::Done);
1091        }
1092        let Some(found) = assets::find(assets, rel, http.header("accept-encoding")) else {
1093            self.refuse(
1094                404,
1095                "assets.none",
1096                EventKind::Refused,
1097                None,
1098                None,
1099                at,
1100                reply,
1101            );
1102            return Some(Outcome::Done);
1103        };
1104        let outcome = assets::serve(assets, &found, http, reply);
1105        self.secure(reply, at.tls, |_| false);
1106        Some(outcome)
1107    }
1108
1109    /// 405 (or 204 for `OPTIONS`) on a path that takes `GET` and `HEAD` only.
1110    fn not_get(&self, method: Method, at: At<'_>, reply: &mut HttpReply) {
1111        if method == Method::Options {
1112            reply.status(204).header("allow", "GET, HEAD, OPTIONS");
1113            self.secure(reply, at.tls, |_| false);
1114        } else {
1115            let (status, code) = if method == Method::Other {
1116                (501, "method.unknown")
1117            } else {
1118                (405, "method.not_allowed")
1119            };
1120            self.refuse(status, code, EventKind::Refused, None, None, at, reply);
1121            if status == 405 {
1122                reply.header("allow", "GET, HEAD, OPTIONS");
1123            }
1124        }
1125    }
1126
1127    fn handle(
1128        &self,
1129        entry: &Entry<G>,
1130        req: Request<'_>,
1131        who: &G::Identity,
1132        caps: Captures,
1133        out: &mut Out,
1134        reply: &mut HttpReply,
1135    ) -> Outcome {
1136        let at = At {
1137            env: req.env,
1138            tls: req.tls,
1139        };
1140        let route = req.route;
1141        let result = {
1142            let mut cx = Cx {
1143                req,
1144                who,
1145                caps,
1146                names: &entry.names,
1147                out: &mut *out,
1148                shared: &self.shared,
1149                template: entry.template,
1150            };
1151            catch_unwind(AssertUnwindSafe(|| -> Result {
1152                for h in entry.routed.iter() {
1153                    h(&mut cx)?;
1154                }
1155                entry.handler.call(&mut cx)?;
1156                for h in entry.finish.iter() {
1157                    h(&mut cx)?;
1158                }
1159                Ok(())
1160            }))
1161        };
1162        match result {
1163            Ok(Ok(())) if !out.invalid => {
1164                reply.status(out.status);
1165                for &(ns, ne, vs, ve) in &out.spans {
1166                    let name = core::str::from_utf8(&out.names[ns as usize..ne as usize])
1167                        .unwrap_or("x-invalid");
1168                    reply.header(name, &out.values[vs as usize..ve as usize]);
1169                }
1170                if !out.has("x-content-type-options") {
1171                    reply.header("x-content-type-options", "nosniff");
1172                }
1173                self.secure(reply, at.tls, |n| out.has(n));
1174                reply.body(&out.body);
1175                Outcome::Done
1176            }
1177            Ok(Ok(())) => {
1178                out.reset();
1179                self.refuse(
1180                    500,
1181                    "reply.invalid_field",
1182                    EventKind::Failed,
1183                    route,
1184                    None,
1185                    at,
1186                    reply,
1187                );
1188                Outcome::Done
1189            }
1190            Ok(Err(e)) => {
1191                out.reset();
1192                let src = std::error::Error::source(&e);
1193                self.refuse(
1194                    e.status(),
1195                    e.code(),
1196                    EventKind::Failed,
1197                    route,
1198                    src,
1199                    at,
1200                    reply,
1201                );
1202                Outcome::Done
1203            }
1204            Err(_) => {
1205                out.reset();
1206                self.refuse(
1207                    500,
1208                    "handler.panic",
1209                    EventKind::Panicked,
1210                    route,
1211                    None,
1212                    at,
1213                    reply,
1214                );
1215                Outcome::Done
1216            }
1217        }
1218    }
1219
1220    /// A gate's denial as a response.
1221    fn deny(&self, d: Denial, req: &Request<'_>, reply: &mut HttpReply) {
1222        let env = req.env;
1223        let at = At { env, tls: req.tls };
1224        if d.kind() == DenialKind::SignIn {
1225            if let Some(page) = &self.shared.sign_in {
1226                if matches!(req.method(), Method::Get | Method::Head) {
1227                    let mut loc = String::with_capacity(64);
1228                    loc.push_str(&self.shared.base);
1229                    loc.push_str(page);
1230                    loc.push_str(if page.contains('?') {
1231                        "&next="
1232                    } else {
1233                        "?next="
1234                    });
1235                    let mut next = String::new();
1236                    next.push_str(&self.shared.base);
1237                    next.push_str(req.path);
1238                    if let Some(q) = req.query() {
1239                        next.push('?');
1240                        next.push_str(q);
1241                    }
1242                    encode_component(&next, &mut loc);
1243                    self.event(EventKind::Denied, env, req.route, 303, d.code(), None);
1244                    reply.status(303).header("location", loc.as_bytes());
1245                    self.secure(reply, at.tls, |_| false);
1246                    return;
1247                }
1248            }
1249        }
1250        let status = d.status_code();
1251        self.refuse(
1252            status,
1253            d.code(),
1254            EventKind::Denied,
1255            req.route,
1256            None,
1257            at,
1258            reply,
1259        );
1260        if status == 401 {
1261            reply.header("www-authenticate", self.shared.challenge.as_bytes());
1262        }
1263    }
1264
1265    fn event(
1266        &self,
1267        kind: EventKind,
1268        env: &Envelope,
1269        route: Option<&str>,
1270        status: u16,
1271        code: &'static str,
1272        source: Option<&(dyn std::error::Error + 'static)>,
1273    ) {
1274        self.shared.events.event(&Event {
1275            kind,
1276            id: RequestId::of(env),
1277            route,
1278            status,
1279            code,
1280            source,
1281        });
1282    }
1283
1284    /// The one function every refusal and error goes through: the status,
1285    /// the error template for it or the built-in page, the reference; the
1286    /// code and source to the events only.
1287    #[allow(clippy::too_many_arguments)]
1288    fn refuse(
1289        &self,
1290        status: u16,
1291        code: &'static str,
1292        kind: EventKind,
1293        route: Option<&str>,
1294        source: Option<&(dyn std::error::Error + 'static)>,
1295        at: At<'_>,
1296        reply: &mut HttpReply,
1297    ) {
1298        self.event(kind, at.env, route, status, code, source);
1299        let reference = RequestId::of(at.env).to_string();
1300        let mut body = Vec::with_capacity(512);
1301        let mut done = false;
1302        if let (Some(engine), Some(&(_, t))) = (
1303            self.shared.render.as_deref(),
1304            self.shared.errors.iter().find(|(s, _)| *s == status),
1305        ) {
1306            let fields = [
1307                ("status", Value::Int(i64::from(status))),
1308                ("title", Value::Text(error::title(status))),
1309                ("message", Value::Text(error::message(status))),
1310                ("reference", Value::Text(&reference)),
1311            ];
1312            let view = Fields(&fields);
1313            let mut out = Output::new(&mut body, self.shared.page_limit);
1314            if engine.render(t, &view, &mut out).is_ok() {
1315                done = true;
1316            } else {
1317                body.clear();
1318            }
1319        }
1320        if !done {
1321            error::builtin_page(status, &reference, &mut body);
1322        }
1323        reply
1324            .status(status)
1325            .header("content-type", "text/html; charset=utf-8")
1326            .header("x-content-type-options", "nosniff")
1327            .header("cache-control", "no-store")
1328            .body(&body);
1329        self.secure(reply, at.tls, |_| false);
1330    }
1331
1332    /// Adds the policy's security headers, except those `has` says the reply
1333    /// set already.
1334    fn secure(&self, reply: &mut HttpReply, tls: bool, has: impl Fn(&str) -> bool) {
1335        for (name, value) in self.shared.security.fields(tls) {
1336            if !has(name) {
1337                reply.header(name, value.as_bytes());
1338            }
1339        }
1340    }
1341
1342    /// Whether HTTPS-only headers apply: the request came over HTTPS and is
1343    /// not for a development host.
1344    fn tls(&self, env: &Envelope, authority: Option<&str>) -> bool {
1345        if env.client().scheme() != Scheme::Https {
1346            return false;
1347        }
1348        let a = authority.unwrap_or("").to_ascii_lowercase();
1349        !(self.site.development && is_dev_host(split_port(a.trim_end_matches('.')).0))
1350    }
1351}
1352
1353impl<G: Gate> HttpService for App<G> {
1354    fn call(&self, request: &HttpRequest<'_>, env: &Envelope, reply: &mut HttpReply) -> Outcome {
1355        self.respond(request, env, reply)
1356    }
1357}
1358
1359impl<G: Gate> fmt::Debug for App<G> {
1360    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1361        f.debug_struct("App")
1362            .field("site", &self.site)
1363            .field("base", &self.shared.base)
1364            .field("routes", &self.entries.len())
1365            .finish_non_exhaustive()
1366    }
1367}
1368
1369/// Percent-encodes a query component: everything but unreserved and `/`.
1370fn encode_component(s: &str, out: &mut String) {
1371    const HEX: &[u8; 16] = b"0123456789ABCDEF";
1372    for b in s.bytes() {
1373        if b.is_ascii_alphanumeric() || matches!(b, b'-' | b'.' | b'_' | b'~' | b'/') {
1374            out.push(b as char);
1375        } else {
1376            out.push('%');
1377            out.push(HEX[(b >> 4) as usize] as char);
1378            out.push(HEX[(b & 15) as usize] as char);
1379        }
1380    }
1381}
1382
1383#[cfg(test)]
1384mod tests {
1385    use super::*;
1386
1387    #[test]
1388    fn test_split_port() {
1389        assert_eq!(split_port("a.test:80"), ("a.test", Some("80")));
1390        assert_eq!(split_port("a.test"), ("a.test", None));
1391        assert_eq!(split_port("[::1]:8080"), ("[::1]", Some("8080")));
1392        assert_eq!(split_port("[::1]"), ("[::1]", None));
1393    }
1394
1395    #[test]
1396    fn test_valid_host() {
1397        for ok in ["example.com", "localhost:8080", "[::1]", "127.0.0.1:80"] {
1398            assert!(valid_host(ok), "{ok}");
1399        }
1400        for bad in ["", "a b", "a:", "a:x", "[zz]", "a/b"] {
1401            assert!(!valid_host(bad), "{bad}");
1402        }
1403    }
1404
1405    #[test]
1406    fn test_selector() {
1407        assert!(Routes::prefix("/blog").matches("/blog", None));
1408        assert!(Routes::prefix("/blog/").matches("/blog/:slug", None));
1409        assert!(!Routes::prefix("/blog").matches("/blogger", None));
1410        assert!(Routes::named(&["a"]).matches("/x", Some("a")));
1411        assert!(!Routes::named(&["a"]).matches("/x", None));
1412    }
1413}