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    Context, Denial, DenialKind, ErrorPage, Event, EventKind, Events, Gate, Identity, NoEvents,
28    NoRender, Open, Output, Render, Request, RequestId, Result, Stage, TemplateId,
29};
30
31/// The default budget for one rendered page.
32///
33/// ```
34/// assert_eq!(web_net::DEFAULT_PAGE_LIMIT, 8 * 1024 * 1024);
35/// ```
36pub const DEFAULT_PAGE_LIMIT: usize = 8 << 20;
37
38/// A named point in the pipeline where registered code runs, only for the
39/// routes it registered for. A route with no hooks pays nothing.
40///
41/// ```
42/// use web_net::{App, Error, Hook, Routes, get};
43///
44/// // A `Routed` hook runs before the handler; its error ends the request.
45/// let app = App::builder()
46///     .hook(Hook::Routed, Routes::prefix("/beta"), |_cx| Err(Error::forbidden("beta.closed")))
47///     .route(get("/beta/news", |cx| cx.text(200, "news")))
48///     .build()?;
49/// let r = serve_net::testing::respond(&app, b"GET /beta/news HTTP/1.1\r\nHost: localhost\r\n\r\n");
50/// assert_eq!(r.status, 403);
51/// # Ok::<(), web_net::BuildError>(())
52/// ```
53#[non_exhaustive]
54#[derive(Clone, Copy, Debug, PartialEq, Eq)]
55pub enum Hook {
56    /// After the access check passed, before the handler: locale, feature
57    /// flags, an A/B variant. An error ends the request.
58    Routed,
59    /// After the handler succeeded, before the reply leaves: add headers.
60    /// An error replaces the reply with the error page.
61    Finish,
62    /// Inside [`Context::render`], before the engine runs: choose a variant, add
63    /// headers. A hook that renders does not run these hooks again.
64    BeforeRender,
65}
66
67/// Which routes a hook runs for.
68///
69/// ```
70/// use web_net::Routes;
71///
72/// let every = Routes::all();
73/// let blog = Routes::prefix("/blog");
74/// let pages = Routes::named(&["home", "about"]);
75/// # let _ = (every, blog, pages);
76/// ```
77#[derive(Clone, Debug)]
78pub struct Routes(Selector);
79
80#[derive(Clone, Debug)]
81enum Selector {
82    All,
83    Prefix(String),
84    Named(Vec<&'static str>),
85}
86
87impl Routes {
88    /// Every route.
89    ///
90    /// ```
91    /// use web_net::{App, Hook, Routes, get};
92    ///
93    /// let app = App::builder()
94    ///     .hook(Hook::Finish, Routes::all(), |cx| {
95    ///         cx.set_header("x-site", "demo");
96    ///         Ok(())
97    ///     })
98    ///     .route(get("/", |cx| cx.text(200, "home")))
99    ///     .build()?;
100    /// let r = serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n");
101    /// assert_eq!(r.header("x-site"), Some(&b"demo"[..]));
102    /// # Ok::<(), web_net::BuildError>(())
103    /// ```
104    #[must_use]
105    pub fn all() -> Self {
106        Self(Selector::All)
107    }
108
109    /// Routes whose pattern starts with `prefix` (`/blog`).
110    ///
111    /// ```
112    /// use serve_net::testing::respond;
113    /// use web_net::{App, Hook, Routes, get};
114    ///
115    /// let app = App::builder()
116    ///     .hook(Hook::Finish, Routes::prefix("/docs"), |cx| {
117    ///         cx.set_header("x-section", "docs");
118    ///         Ok(())
119    ///     })
120    ///     .route(get("/docs/intro", |cx| cx.text(200, "intro")))
121    ///     .route(get("/docsearch", |cx| cx.text(200, "search")))
122    ///     .build()?;
123    /// let r = respond(&app, b"GET /docs/intro HTTP/1.1\r\nHost: localhost\r\n\r\n");
124    /// assert_eq!(r.header("x-section"), Some(&b"docs"[..]));
125    /// // A prefix matches whole segments.
126    /// let r = respond(&app, b"GET /docsearch HTTP/1.1\r\nHost: localhost\r\n\r\n");
127    /// assert_eq!(r.header("x-section"), None);
128    /// # Ok::<(), web_net::BuildError>(())
129    /// ```
130    #[must_use]
131    pub fn prefix(prefix: impl Into<String>) -> Self {
132        Self(Selector::Prefix(prefix.into()))
133    }
134
135    /// Routes with one of these names.
136    ///
137    /// ```
138    /// use web_net::{App, Hook, Routes, get};
139    ///
140    /// let app = App::builder()
141    ///     .hook(Hook::Finish, Routes::named(&["home", "about"]), |cx| {
142    ///         cx.set_header("x-page", "static");
143    ///         Ok(())
144    ///     })
145    ///     .route(get("/", |cx| cx.text(200, "home")).name("home"))
146    ///     .build()?;
147    /// let r = serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n");
148    /// assert_eq!(r.header("x-page"), Some(&b"static"[..]));
149    /// # Ok::<(), web_net::BuildError>(())
150    /// ```
151    #[must_use]
152    pub fn named(names: &[&'static str]) -> Self {
153        Self(Selector::Named(names.to_vec()))
154    }
155
156    fn matches(&self, pattern: &str, name: Option<&str>) -> bool {
157        match &self.0 {
158            Selector::All => true,
159            Selector::Prefix(p) => {
160                let p = p.trim_end_matches('/');
161                pattern == p
162                    || pattern
163                        .strip_prefix(p)
164                        .is_some_and(|rest| rest.starts_with('/'))
165            }
166            Selector::Named(names) => name.is_some_and(|n| names.contains(&n)),
167        }
168    }
169}
170
171pub(crate) type HookFn<I, E> = Arc<dyn Fn(&mut Context<'_, I, E>) -> Result + Send + Sync>;
172
173/// What [`App::classify`] tells a dispatcher before admission.
174///
175/// ```
176/// use web_net::{App, get};
177/// use serve_net::{Envelope, HttpReply, HttpRequest, HttpService, Outcome};
178///
179/// // A dispatcher in front of the app that answers with what `classify` said.
180/// struct Probe(App);
181/// impl HttpService for Probe {
182///     fn call(&self, req: &HttpRequest<'_>, _: &Envelope, reply: &mut HttpReply) -> Outcome {
183///         let c = self.0.classify(req.head());
184///         reply.text(200, &format!("{:?} {:?}", c.route, c.work));
185///         Outcome::Done
186///     }
187/// }
188///
189/// let app = App::builder().route(get("/", |cx| cx.text(200, "hi"))).build()?;
190/// let r = serve_net::testing::respond(&Probe(app), b"GET /none HTTP/1.1\r\nHost: localhost\r\n\r\n");
191/// assert_eq!(r.text(), "None Quick");
192/// # Ok::<(), web_net::BuildError>(())
193/// ```
194#[non_exhaustive]
195#[derive(Clone, Copy, Debug, PartialEq, Eq)]
196pub struct Classified {
197    /// The route that will run, if one matched.
198    pub route: Option<RouteId>,
199    /// How its handler behaves (`Quick` when nothing matched).
200    pub work: WorkClass,
201}
202
203/// A declared route (one method of one pattern), numbered in declaration
204/// order.
205///
206/// ```
207/// use web_net::{App, get};
208/// use serve_net::{Envelope, HttpReply, HttpRequest, HttpService, Outcome};
209///
210/// // A dispatcher in front of the app that answers with what `classify` said.
211/// struct Probe(App);
212/// impl HttpService for Probe {
213///     fn call(&self, req: &HttpRequest<'_>, _: &Envelope, reply: &mut HttpReply) -> Outcome {
214///         let c = self.0.classify(req.head());
215///         reply.text(200, &format!("{:?} {:?}", c.route, c.work));
216///         Outcome::Done
217///     }
218/// }
219///
220/// let app = App::builder()
221///     .route(get("/", |cx| cx.text(200, "home")))
222///     .route(get("/about", |cx| cx.text(200, "about")))
223///     .build()?;
224/// let r = serve_net::testing::respond(&Probe(app), b"GET /about HTTP/1.1\r\nHost: localhost\r\n\r\n");
225/// assert_eq!(r.text(), "Some(RouteId(1)) Quick");
226/// # Ok::<(), web_net::BuildError>(())
227/// ```
228#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
229pub struct RouteId(pub u32);
230
231/// The parts of the application every handler can reach.
232pub(crate) struct Shared<E: Render> {
233    pub(crate) render: Option<E>,
234    pub(crate) checkpoint: Option<Box<dyn crate::Checkpoint>>,
235    pub(crate) events: Box<dyn Events>,
236    pub(crate) page_limit: usize,
237    pub(crate) base: Box<str>,
238    sign_in: Option<Box<str>>,
239    challenge: Box<str>,
240    wipe: bool,
241    security: SecurityHeaders,
242    pub(crate) csrf: Option<csrf_sec::Csrf>,
243    pub(crate) redirect_hosts: Vec<Box<str>>,
244    compression: Option<Compression>,
245}
246
247/// Where a reply is going: the request's envelope, and whether security
248/// headers for HTTPS apply (HTTPS, and not a development host).
249#[derive(Clone, Copy)]
250pub(crate) struct At<'a> {
251    pub(crate) env: &'a Envelope,
252    pub(crate) tls: bool,
253    /// The client prefers JSON to HTML: error replies are JSON.
254    pub(crate) json: bool,
255}
256
257type ReadyFn = Arc<dyn Fn() -> bool + Send + Sync>;
258
259#[derive(Clone)]
260enum Health {
261    Live,
262    Ready(ReadyFn),
263}
264
265#[derive(Clone, Debug, Default)]
266struct Site {
267    /// Lower case; the first is canonical.
268    hosts: Vec<Box<str>>,
269    aliases: Vec<Box<str>>,
270    development: bool,
271    /// Any `Host` is the site (`Builder::any_host`); without it and without
272    /// declared hosts, only development hosts are.
273    any_host: bool,
274    slash: TrailingSlash,
275    lowercase: bool,
276    https_only: bool,
277}
278
279struct Entry<G: Gate, E: Render> {
280    handler: BoxHandler<G::Identity, E>,
281    access: Access<G::Rule>,
282    names: Box<[Box<str>]>,
283    pattern: Box<str>,
284    work: WorkClass,
285    template: Option<TemplateId>,
286    csrf: bool,
287    limits: form_net::Limits,
288    cors: Option<Arc<Cors>>,
289    cache: CachePolicy,
290    /// `Cache-Control` for guests and for signed-in visitors, made once.
291    cache_values: (Option<Box<str>>, Option<Box<str>>),
292    routed: Box<[HookFn<G::Identity, E>]>,
293    finish: Box<[HookFn<G::Identity, E>]>,
294    before_render: Box<[HookFn<G::Identity, E>]>,
295}
296
297struct Endpoint {
298    /// Entry per method, by `Method::index`.
299    methods: [Option<u32>; 10],
300    allow: Box<str>,
301    set: MethodSet,
302}
303
304/// A web application: one site, its routes, and its gate. Built once at
305/// start-up with [`App::builder`] or [`App::with_gate`]; immutable after.
306///
307/// [`listen`](Self::listen) serves it on an address. It is also a
308/// `serve_net::HttpService`, so it runs on any serve-net server as it is,
309/// and [`respond`](Self::respond) runs it from a wrapper's own service.
310///
311/// ```
312/// use web_net::{App, get};
313///
314/// let app = App::builder()
315///     .route(get("/", |cx| cx.html(200, "<h1>Hello</h1>")))
316///     .route(get("/users/:id", |cx| {
317///         let id: u64 = cx.param_as("id")?;
318///         cx.text(200, &format!("user {id}"))
319///     }))
320///     .build()?;
321///
322/// let reply = serve_net::testing::respond(&app, b"GET /users/7 HTTP/1.1\r\nHost: localhost\r\n\r\n");
323/// assert_eq!((reply.status, reply.text()), (200, "user 7"));
324///
325/// let reply = serve_net::testing::respond(&app, b"GET /users/7/ HTTP/1.1\r\nHost: localhost\r\n\r\n");
326/// assert_eq!(reply.status, 301);
327/// assert_eq!(reply.header("location"), Some(&b"/users/7"[..]));
328/// # Ok::<(), web_net::BuildError>(())
329/// ```
330pub struct App<G: Gate = Open, E: Render = NoRender> {
331    gate: G,
332    shared: Shared<E>,
333    site: Site,
334    trie: Trie,
335    endpoints: Vec<Endpoint>,
336    entries: Vec<Entry<G, E>>,
337    allow_all: Box<str>,
338    assets: Vec<Assets>,
339    health: Vec<(Box<str>, Health)>,
340    streams: assets::Streams,
341}
342
343impl App<Open> {
344    /// A builder with the [`Open`] gate: every visitor is a guest and every
345    /// route is public, so routes declare no access.
346    ///
347    /// ```
348    /// use web_net::{App, get};
349    ///
350    /// let app = App::builder().route(get("/", |cx| cx.text(200, "hi"))).build()?;
351    /// let r = serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n");
352    /// assert_eq!((r.status, r.text()), (200, "hi"));
353    /// # Ok::<(), web_net::BuildError>(())
354    /// ```
355    #[must_use]
356    pub fn builder() -> Builder<Open> {
357        Builder::new(Open)
358    }
359}
360
361impl<G: Gate> App<G> {
362    /// A builder with `gate` for authentication and authorization. Every
363    /// route then declares its access, [`public`](crate::Route::public) or
364    /// [`allow(rule)`](crate::Route::allow), or the build fails.
365    ///
366    /// ```
367    /// use serve_net::testing::respond;
368    /// use web_net::{App, Denial, Gate, Guest, Request, get};
369    ///
370    /// enum Rule { Staff }
371    /// struct NoStaff;
372    /// impl Gate for NoStaff {
373    ///     type Identity = Guest;
374    ///     type Rule = Rule;
375    ///     fn authenticate(&self, _: &Request<'_>) -> Result<Guest, Denial> { Ok(Guest) }
376    ///     fn authorize(&self, _: &Guest, _: &Rule, _: &Request<'_>) -> Result<(), Denial> {
377    ///         Err(Denial::forbidden("staff.only"))
378    ///     }
379    /// }
380    ///
381    /// // A route that declares no access stops the build.
382    /// assert!(App::with_gate(NoStaff).route(get("/", |cx| cx.text(200, "hi"))).build().is_err());
383    ///
384    /// let app = App::with_gate(NoStaff)
385    ///     .route(get("/", |cx| cx.text(200, "hi")).public())
386    ///     .route(get("/admin", |cx| cx.text(200, "admin")).allow(Rule::Staff))
387    ///     .build()?;
388    /// assert_eq!(respond(&app, b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n").status, 200);
389    /// assert_eq!(respond(&app, b"GET /admin HTTP/1.1\r\nHost: localhost\r\n\r\n").status, 403);
390    /// # Ok::<(), web_net::BuildError>(())
391    /// ```
392    #[must_use]
393    pub fn with_gate(gate: G) -> Builder<G> {
394        Builder::new(gate)
395    }
396}
397
398/// Declares an application. Every mistake is reported by
399/// [`build`](Self::build) at once, before anything listens.
400///
401/// ```
402/// use web_net::{App, get};
403///
404/// let app = App::builder()
405///     .host("example.com")
406///     .development()
407///     .health("/healthz")
408///     .route(get("/", |cx| cx.text(200, "home")))
409///     .build()?;
410/// let r = serve_net::testing::respond(&app, b"GET /healthz HTTP/1.1\r\nHost: localhost\r\n\r\n");
411/// assert_eq!(r.text(), "ok");
412/// # Ok::<(), web_net::BuildError>(())
413/// ```
414pub struct Builder<G: Gate, E: Render = NoRender> {
415    gate: G,
416    routes: Vec<Route<G::Identity, G::Rule, E>>,
417    hooks: Vec<(Hook, Routes, HookFn<G::Identity, E>)>,
418    site: Site,
419    base: String,
420    render: Option<E>,
421    checkpoint: Option<Box<dyn crate::Checkpoint>>,
422    events: Box<dyn Events>,
423    page_limit: usize,
424    sign_in: Option<String>,
425    challenge: String,
426    wipe: bool,
427    security: SecurityHeaders,
428    assets: Vec<Assets>,
429    health: Vec<(String, Health)>,
430    csrf: Option<csrf_sec::Csrf>,
431    redirect_hosts: Vec<Box<str>>,
432    compression: Option<Compression>,
433    /// Problems found before `build` (an engine set after routes).
434    early: Vec<Problem>,
435}
436
437impl<G: Gate, E: Render> Builder<G, E> {
438    fn new(gate: G) -> Self {
439        Self {
440            gate,
441            routes: Vec::new(),
442            hooks: Vec::new(),
443            site: Site::default(),
444            base: String::new(),
445            render: None,
446            checkpoint: None,
447            events: Box::new(NoEvents),
448            page_limit: DEFAULT_PAGE_LIMIT,
449            sign_in: None,
450            challenge: "Session".into(),
451            wipe: false,
452            security: SecurityHeaders::default(),
453            assets: Vec::new(),
454            health: Vec::new(),
455            csrf: None,
456            redirect_hosts: Vec::new(),
457            compression: None,
458            early: Vec::new(),
459        }
460    }
461
462    /// Compress replies with gzip when the client accepts it: HTML, CSS,
463    /// JavaScript, JSON, XML, SVG, and other text, over a minimum size. Off
464    /// by default: a proxy in front usually compresses.
465    ///
466    /// A compressed page that carries a secret beside text the visitor
467    /// chose (a reflected query value) can leak the secret through its
468    /// compressed size (BREACH). csrf-sec masks its tokens per reply; keep
469    /// compression off for pages with other secrets.
470    ///
471    /// ```
472    /// use web_net::{App, Compression, get};
473    ///
474    /// let app = App::builder()
475    ///     .compress(Compression::default())
476    ///     .route(get("/", |cx| cx.text(200, &"hello ".repeat(400))))
477    ///     .build()?;
478    /// let r = serve_net::testing::respond(
479    ///     &app,
480    ///     b"GET / HTTP/1.1\r\nHost: localhost\r\nAccept-Encoding: gzip\r\n\r\n",
481    /// );
482    /// assert_eq!(r.header("content-encoding"), Some(&b"gzip"[..]));
483    /// # Ok::<(), web_net::BuildError>(())
484    /// ```
485    #[must_use]
486    pub fn compress(mut self, compression: Compression) -> Self {
487        self.compression = Some(compression);
488        self
489    }
490
491    /// CSRF protection with csrf-sec (re-exported as
492    /// [`web_net::csrf_sec`](crate::csrf_sec)), built from a persisted
493    /// 32-byte key. Required as soon as one route takes an unsafe method and
494    /// is not [exempt](crate::Route::csrf_exempt).
495    ///
496    /// web-net always applies the origin policy and a token, whatever the
497    /// `Csrf`'s `Mode`; its keys, lifetimes, and allowed origins apply as set.
498    ///
499    /// ```
500    /// use web_net::{App, csrf_sec::Csrf, get, post};
501    ///
502    /// let app = App::builder()
503    ///     .csrf(Csrf::new(*b"a persisted 32-byte secret key!!").unwrap())
504    ///     .route(get("/", |cx| cx.text(200, "form")))
505    ///     .route(post("/", |cx| cx.redirect(303, "/")))
506    ///     .build()?;
507    /// // A POST without a token is refused before its handler runs.
508    /// let r = serve_net::testing::respond(
509    ///     &app,
510    ///     b"POST / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 0\r\n\r\n",
511    /// );
512    /// assert_eq!(r.status, 403);
513    /// # Ok::<(), web_net::BuildError>(())
514    /// ```
515    #[must_use]
516    pub fn csrf(mut self, csrf: csrf_sec::Csrf) -> Self {
517        self.csrf = Some(csrf);
518        self
519    }
520
521    /// Hosts that [`Context::redirect_external`] may send a visitor to
522    /// (`accounts.example.com`). Everything else is refused.
523    ///
524    /// ```
525    /// use web_net::{App, get};
526    ///
527    /// let app = App::builder()
528    ///     .redirect_hosts(&["accounts.example.com"])
529    ///     .route(get("/login", |cx| cx.redirect_external(302, "https://accounts.example.com/login")))
530    ///     .build()?;
531    /// let r = serve_net::testing::respond(&app, b"GET /login HTTP/1.1\r\nHost: localhost\r\n\r\n");
532    /// assert_eq!(r.header("location"), Some(&b"https://accounts.example.com/login"[..]));
533    /// # Ok::<(), web_net::BuildError>(())
534    /// ```
535    #[must_use]
536    pub fn redirect_hosts(mut self, hosts: &[&str]) -> Self {
537        self.redirect_hosts.extend(
538            hosts
539                .iter()
540                .map(|h| h.to_ascii_lowercase().into_boxed_str()),
541        );
542        self
543    }
544
545    /// Serves a folder of files under a mount path, before routing and
546    /// without the gate (files are public).
547    ///
548    /// ```
549    /// use web_net::{App, Assets};
550    ///
551    /// let dir = std::env::temp_dir().join("web-net-doc-builder-assets");
552    /// std::fs::create_dir_all(&dir).unwrap();
553    /// std::fs::write(dir.join("hello.txt"), "hello").unwrap();
554    ///
555    /// let app = App::builder().assets(Assets::new("/static", &dir)).build()?;
556    /// let r = serve_net::testing::respond(&app, b"GET /static/hello.txt HTTP/1.1\r\nHost: localhost\r\n\r\n");
557    /// assert_eq!((r.status, r.text()), (200, "hello"));
558    /// # Ok::<(), web_net::BuildError>(())
559    /// ```
560    #[must_use]
561    pub fn assets(mut self, assets: Assets) -> Self {
562        self.assets.push(assets);
563        self
564    }
565
566    /// A liveness path (`/healthz`): `200 ok` whenever the process answers.
567    /// Answered before routing and without the gate.
568    ///
569    /// ```
570    /// let app = web_net::App::builder().health("/healthz").build()?;
571    /// let r = serve_net::testing::respond(&app, b"GET /healthz HTTP/1.1\r\nHost: localhost\r\n\r\n");
572    /// assert_eq!((r.status, r.text()), (200, "ok"));
573    /// # Ok::<(), web_net::BuildError>(())
574    /// ```
575    #[must_use]
576    pub fn health(mut self, path: &str) -> Self {
577        self.health.push((path.to_owned(), Health::Live));
578        self
579    }
580
581    /// A readiness path (`/readyz`): `200 ready` when `check` says so,
582    /// `503 not ready` otherwise. Keep `check` to a flag read: it runs on the
583    /// request path.
584    ///
585    /// ```
586    /// use std::sync::Arc;
587    /// use std::sync::atomic::{AtomicBool, Ordering};
588    /// use serve_net::testing::respond;
589    ///
590    /// let up = Arc::new(AtomicBool::new(false));
591    /// let flag = Arc::clone(&up);
592    /// let app = web_net::App::builder()
593    ///     .ready("/readyz", move || flag.load(Ordering::Relaxed))
594    ///     .build()?;
595    /// let req = b"GET /readyz HTTP/1.1\r\nHost: localhost\r\n\r\n";
596    /// assert_eq!(respond(&app, req).status, 503);
597    /// up.store(true, Ordering::Relaxed);
598    /// assert_eq!(respond(&app, req).text(), "ready");
599    /// # Ok::<(), web_net::BuildError>(())
600    /// ```
601    #[must_use]
602    pub fn ready(mut self, path: &str, check: impl Fn() -> bool + Send + Sync + 'static) -> Self {
603        self.health
604            .push((path.to_owned(), Health::Ready(Arc::new(check))));
605        self
606    }
607
608    /// The security headers every reply carries (default
609    /// [`SecurityHeaders::default`]).
610    ///
611    /// ```
612    /// use web_net::{App, SecurityHeaders, get};
613    ///
614    /// // A proxy in front sets the security headers, so web-net sends none.
615    /// let app = App::builder()
616    ///     .security_headers(SecurityHeaders::none())
617    ///     .route(get("/", |cx| cx.html(200, "<p>hi</p>")))
618    ///     .build()?;
619    /// let r = serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n");
620    /// assert_eq!(r.header("content-security-policy"), None);
621    /// # Ok::<(), web_net::BuildError>(())
622    /// ```
623    #[must_use]
624    pub fn security_headers(mut self, policy: SecurityHeaders) -> Self {
625        self.security = policy;
626        self
627    }
628
629    /// Adds a route.
630    ///
631    /// ```
632    /// use web_net::{App, get};
633    ///
634    /// let app = App::builder().route(get("/about", |cx| cx.text(200, "about"))).build()?;
635    /// let r = serve_net::testing::respond(&app, b"GET /about HTTP/1.1\r\nHost: localhost\r\n\r\n");
636    /// assert_eq!(r.text(), "about");
637    /// # Ok::<(), web_net::BuildError>(())
638    /// ```
639    #[must_use]
640    pub fn route(mut self, route: Route<G::Identity, G::Rule, E>) -> Self {
641        self.routes.push(route);
642        self
643    }
644
645    /// Adds a group's routes.
646    ///
647    /// ```
648    /// use web_net::{App, Group, get};
649    ///
650    /// let app = App::builder()
651    ///     .group(Group::at("/admin").route(get("/users", |cx| cx.text(200, "users"))))
652    ///     .build()?;
653    /// let r = serve_net::testing::respond(&app, b"GET /admin/users HTTP/1.1\r\nHost: localhost\r\n\r\n");
654    /// assert_eq!(r.text(), "users");
655    /// # Ok::<(), web_net::BuildError>(())
656    /// ```
657    #[must_use]
658    pub fn group(mut self, group: Group<G::Identity, G::Rule, E>) -> Self {
659        self.routes.extend(group.into_routes());
660        self
661    }
662
663    /// Runs `f` at `hook` for the routes `routes` selects.
664    ///
665    /// ```
666    /// use web_net::{App, Hook, Routes, get};
667    ///
668    /// let app = App::builder()
669    ///     .hook(Hook::Finish, Routes::all(), |cx| {
670    ///         cx.set_header("x-served-by", "web-net");
671    ///         Ok(())
672    ///     })
673    ///     .route(get("/", |cx| cx.text(200, "home")))
674    ///     .build()?;
675    /// let r = serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n");
676    /// assert_eq!(r.header("x-served-by"), Some(&b"web-net"[..]));
677    /// # Ok::<(), web_net::BuildError>(())
678    /// ```
679    #[must_use]
680    pub fn hook<F>(mut self, hook: Hook, routes: Routes, f: F) -> Self
681    where
682        F: Fn(&mut Context<'_, G::Identity, E>) -> Result + Send + Sync + 'static,
683    {
684        self.hooks.push((hook, routes, Arc::new(f)));
685        self
686    }
687
688    /// A host the site answers (`example.com`, or `localhost:8080` with its
689    /// port). The first one is canonical; any other host gets 421.
690    ///
691    /// With no host declared, the site answers the development hosts only
692    /// (`localhost`, `127.0.0.1`, `[::1]`, `*.localhost`), so a site works
693    /// on a laptop as it is and a name someone else points at the server
694    /// (DNS rebinding) is never the site. Declare the production host before
695    /// deploying, or [`any_host`](Self::any_host) for a site that must
696    /// answer every name.
697    ///
698    /// ```
699    /// use web_net::{App, get};
700    /// use serve_net::testing::respond;
701    ///
702    /// let app = App::builder().route(get("/", |cx| cx.text(200, "hi"))).build()?;
703    /// assert_eq!(respond(&app, b"GET / HTTP/1.1\r\nHost: localhost:8080\r\n\r\n").status, 200);
704    /// assert_eq!(respond(&app, b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n").status, 421);
705    ///
706    /// let app = App::builder().host("example.com").route(get("/", |cx| cx.text(200, "hi"))).build()?;
707    /// assert_eq!(respond(&app, b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n").status, 200);
708    /// assert_eq!(respond(&app, b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n").status, 421);
709    /// # Ok::<(), web_net::BuildError>(())
710    /// ```
711    #[must_use]
712    pub fn host(mut self, host: &str) -> Self {
713        self.site.hosts.push(host.to_ascii_lowercase().into());
714        self
715    }
716
717    /// A host that is redirected (301, or 308 for unsafe methods) to the
718    /// canonical host (`www.example.com` to `example.com`).
719    ///
720    /// ```
721    /// use web_net::{App, get};
722    ///
723    /// let app = App::builder()
724    ///     .host("example.com")
725    ///     .alias("www.example.com")
726    ///     .route(get("/", |cx| cx.text(200, "home")))
727    ///     .build()?;
728    /// let r = serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n");
729    /// assert_eq!((r.status, r.header("location")), (301, Some(&b"http://example.com/"[..])));
730    /// # Ok::<(), web_net::BuildError>(())
731    /// ```
732    #[must_use]
733    pub fn alias(mut self, host: &str) -> Self {
734        self.site.aliases.push(host.to_ascii_lowercase().into());
735        self
736    }
737
738    /// A development instance of a site with declared hosts: `localhost`,
739    /// `127.0.0.1`, `[::1]`, and `*.localhost` are answered as the site too
740    /// (with any port), and HTTPS is not required for them. Never set this
741    /// in production. (A site with no declared host answers only these
742    /// already.)
743    ///
744    /// ```
745    /// use web_net::{App, get};
746    ///
747    /// let app = App::builder()
748    ///     .host("example.com")
749    ///     .development()
750    ///     .route(get("/", |cx| cx.text(200, "home")))
751    ///     .build()?;
752    /// let r = serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: localhost:8080\r\n\r\n");
753    /// assert_eq!(r.status, 200);
754    /// # Ok::<(), web_net::BuildError>(())
755    /// ```
756    #[must_use]
757    pub fn development(mut self) -> Self {
758        self.site.development = true;
759        self
760    }
761
762    /// Answers every `Host`, for a site behind a front that checked the
763    /// host already, or one that is reached by names it cannot list. Not
764    /// with [`host`](Self::host).
765    ///
766    /// ```
767    /// use web_net::{App, get};
768    ///
769    /// let app = App::builder().any_host().route(get("/", |cx| cx.text(200, "hi"))).build()?;
770    /// let r = serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: anything.example\r\n\r\n");
771    /// assert_eq!(r.status, 200);
772    /// # Ok::<(), web_net::BuildError>(())
773    /// ```
774    #[must_use]
775    pub fn any_host(mut self) -> Self {
776        self.site.any_host = true;
777        self
778    }
779
780    /// The path the site is installed under (`/blog`). It is removed before
781    /// routing and added back by [`Context::url`], so routes and links are written
782    /// as if the site were at `/`. Requests outside it get 404.
783    ///
784    /// ```
785    /// use serve_net::testing::respond;
786    /// use web_net::{App, get};
787    ///
788    /// let app = App::builder()
789    ///     .base("/blog")
790    ///     .route(get("/posts", |cx| cx.text(200, "posts")))
791    ///     .build()?;
792    /// assert_eq!(respond(&app, b"GET /blog/posts HTTP/1.1\r\nHost: localhost\r\n\r\n").text(), "posts");
793    /// assert_eq!(respond(&app, b"GET /posts HTTP/1.1\r\nHost: localhost\r\n\r\n").status, 404);
794    /// # Ok::<(), web_net::BuildError>(())
795    /// ```
796    #[must_use]
797    pub fn base(mut self, base: &str) -> Self {
798        self.base = base.to_owned();
799        self
800    }
801
802    /// The trailing-slash policy (default [`TrailingSlash::Strip`]).
803    ///
804    /// ```
805    /// use web_net::{App, TrailingSlash, get};
806    ///
807    /// // Both spellings reach the route and neither is redirected.
808    /// let app = App::builder()
809    ///     .trailing_slash(TrailingSlash::Ignore)
810    ///     .route(get("/docs", |cx| cx.text(200, "docs")))
811    ///     .build()?;
812    /// let r = serve_net::testing::respond(&app, b"GET /docs/ HTTP/1.1\r\nHost: localhost\r\n\r\n");
813    /// assert_eq!((r.status, r.text()), (200, "docs"));
814    /// # Ok::<(), web_net::BuildError>(())
815    /// ```
816    #[must_use]
817    pub fn trailing_slash(mut self, policy: TrailingSlash) -> Self {
818        self.site.slash = policy;
819        self
820    }
821
822    /// Paths are lower case: one with an upper-case letter is redirected.
823    ///
824    /// ```
825    /// use web_net::{App, get};
826    ///
827    /// let app = App::builder()
828    ///     .lowercase_paths()
829    ///     .route(get("/about", |cx| cx.text(200, "about")))
830    ///     .build()?;
831    /// let r = serve_net::testing::respond(&app, b"GET /About HTTP/1.1\r\nHost: localhost\r\n\r\n");
832    /// assert_eq!((r.status, r.header("location")), (301, Some(&b"/about"[..])));
833    /// # Ok::<(), web_net::BuildError>(())
834    /// ```
835    #[must_use]
836    pub fn lowercase_paths(mut self) -> Self {
837        self.site.lowercase = true;
838        self
839    }
840
841    /// Plain HTTP is redirected to HTTPS on the canonical host (behind a
842    /// TLS proxy, serve-net must trust the proxy's scheme). Needs a host.
843    ///
844    /// ```
845    /// use web_net::{App, get};
846    ///
847    /// let app = App::builder()
848    ///     .host("example.com")
849    ///     .https_only()
850    ///     .route(get("/", |cx| cx.text(200, "home")))
851    ///     .build()?;
852    /// let r = serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n");
853    /// assert_eq!((r.status, r.header("location")), (301, Some(&b"https://example.com/"[..])));
854    /// # Ok::<(), web_net::BuildError>(())
855    /// ```
856    #[must_use]
857    pub fn https_only(mut self) -> Self {
858        self.site.https_only = true;
859        self
860    }
861
862    /// The site-relative sign-in page. A [`Denial::sign_in`] on a `GET` or
863    /// `HEAD` is then answered with a 303 to it, the page asked for passed
864    /// as `next`; without it, or for other methods, the answer is 401.
865    ///
866    /// ```
867    /// use web_net::{App, get};
868    /// # use web_net::{Denial, Gate, Guest, Request};
869    /// # enum Rule { Member }
870    /// # struct Members;
871    /// # impl Gate for Members {
872    /// #     type Identity = Guest;
873    /// #     type Rule = Rule;
874    /// #     fn authenticate(&self, _: &Request<'_>) -> Result<Guest, Denial> { Ok(Guest) }
875    /// #     fn authorize(&self, _: &Guest, _: &Rule, _: &Request<'_>) -> Result<(), Denial> {
876    /// #         Err(Denial::sign_in("session.none"))
877    /// #     }
878    /// # }
879    /// // `Members` answers `Rule::Member` with `Denial::sign_in` for everyone.
880    /// let app = App::with_gate(Members)
881    ///     .sign_in("/login")
882    ///     .route(get("/login", |cx| cx.text(200, "sign in")).public())
883    ///     .route(get("/account", |cx| cx.text(200, "account")).allow(Rule::Member))
884    ///     .build()?;
885    /// let r = serve_net::testing::respond(&app, b"GET /account HTTP/1.1\r\nHost: localhost\r\n\r\n");
886    /// assert_eq!((r.status, r.header("location")), (303, Some(&b"/login?next=/account"[..])));
887    /// # Ok::<(), web_net::BuildError>(())
888    /// ```
889    #[must_use]
890    pub fn sign_in(mut self, path: &str) -> Self {
891        self.sign_in = Some(path.to_owned());
892        self
893    }
894
895    /// The `WWW-Authenticate` value sent with a 401 (default `Session`).
896    ///
897    /// ```
898    /// let app = web_net::App::builder().www_authenticate("Bearer").build()?;
899    /// # Ok::<(), web_net::BuildError>(())
900    /// ```
901    #[must_use]
902    pub fn www_authenticate(mut self, challenge: &str) -> Self {
903        challenge.clone_into(&mut self.challenge);
904        self
905    }
906
907    /// Where refusals, denials, and failures are reported.
908    ///
909    /// ```
910    /// use std::sync::Arc;
911    /// use std::sync::atomic::{AtomicU32, Ordering};
912    /// use web_net::{App, Error, Event, EventKind, Events, get};
913    ///
914    /// #[derive(Default)]
915    /// struct Failures(AtomicU32);
916    /// impl Events for Failures {
917    ///     fn event(&self, e: &Event<'_>) {
918    ///         if e.kind == EventKind::Failed {
919    ///             self.0.fetch_add(1, Ordering::Relaxed);
920    ///         }
921    ///     }
922    /// }
923    ///
924    /// let failures = Arc::new(Failures::default());
925    /// let app = App::builder()
926    ///     .events(Arc::clone(&failures))
927    ///     .route(get("/", |_cx| Err(Error::unavailable("db.down"))))
928    ///     .build()?;
929    /// serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n");
930    /// assert_eq!(failures.0.load(Ordering::Relaxed), 1);
931    /// # Ok::<(), web_net::BuildError>(())
932    /// ```
933    #[must_use]
934    pub fn events(mut self, events: impl Events) -> Self {
935        self.events = Box::new(events);
936        self
937    }
938
939    /// The in-flight revocation check at web-net's stages.
940    ///
941    /// ```
942    /// use serve_net::{Envelope, Verdict};
943    /// use web_net::{App, Checkpoint, Stage, get};
944    ///
945    /// struct Maintenance;
946    /// impl Checkpoint for Maintenance {
947    ///     fn check(&self, _: &Envelope, _: Stage) -> Verdict {
948    ///         Verdict::Deny { status: 503 }
949    ///     }
950    /// }
951    ///
952    /// let app = App::builder()
953    ///     .checkpoint(Maintenance)
954    ///     .route(get("/", |cx| cx.text(200, "hi")))
955    ///     .build()?;
956    /// let r = serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n");
957    /// assert_eq!(r.status, 503);
958    /// # Ok::<(), web_net::BuildError>(())
959    /// ```
960    #[must_use]
961    pub fn checkpoint(mut self, checkpoint: impl crate::Checkpoint) -> Self {
962        self.checkpoint = Some(Box::new(checkpoint));
963        self
964    }
965
966    /// The budget for one rendered page (default 8 MiB).
967    ///
968    /// ```
969    /// use web_net::{App, Context, Guest, get};
970    /// # use web_net::{Output, Render, RenderError, TemplateId};
971    /// # struct Pages;
972    /// # impl Render for Pages {
973    /// #     type View<'a> = str;
974    /// #     fn template(&self, name: &str) -> Option<TemplateId> {
975    /// #         (name == "page").then_some(TemplateId(0))
976    /// #     }
977    /// #     fn render(&self, _: TemplateId, text: &str, out: &mut Output<'_>) -> Result<(), RenderError> {
978    /// #         out.write_str(text)
979    /// #     }
980    /// # }
981    /// // `Pages` is an engine whose one template, "page", writes the view (a `str`).
982    /// let app = App::builder()
983    ///     .render(Pages)
984    ///     .page_limit(4)
985    ///     .route(get("/", |cx: &mut Context<'_, Guest, Pages>| cx.render("more than four bytes")).template("page"))
986    ///     .build()?;
987    /// let r = serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n");
988    /// assert_eq!(r.status, 500);
989    /// # Ok::<(), web_net::BuildError>(())
990    /// ```
991    #[must_use]
992    pub fn page_limit(mut self, bytes: usize) -> Self {
993        self.page_limit = bytes;
994        self
995    }
996
997    /// Zero the reply buffers web-net reuses after every request, so one
998    /// visitor's page never lingers in memory another request reuses. Costs
999    /// one pass over the bytes written.
1000    ///
1001    /// ```
1002    /// use web_net::{App, get};
1003    ///
1004    /// let app = App::builder()
1005    ///     .wipe_buffers()
1006    ///     .route(get("/", |cx| cx.text(200, "a private page")))
1007    ///     .build()?;
1008    /// let r = serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n");
1009    /// assert_eq!(r.text(), "a private page");
1010    /// # Ok::<(), web_net::BuildError>(())
1011    /// ```
1012    #[must_use]
1013    pub fn wipe_buffers(mut self) -> Self {
1014        self.wipe = true;
1015        self
1016    }
1017
1018    /// Compiles the application.
1019    ///
1020    /// # Errors
1021    ///
1022    /// A [`BuildError`] listing every problem: a route without declared
1023    /// access, a malformed pattern, two handlers for one method of one
1024    /// path, an unknown template, a malformed host or base path.
1025    ///
1026    /// ```
1027    /// use web_net::{App, get};
1028    ///
1029    /// assert!(App::builder().route(get("/", |cx| cx.text(200, "hi"))).build().is_ok());
1030    ///
1031    /// // Every problem is listed, not only the first.
1032    /// let err = App::builder()
1033    ///     .base("blog")
1034    ///     .route(get("/a", |cx| cx.text(200, "a")))
1035    ///     .route(get("/a", |cx| cx.text(200, "again")))
1036    ///     .build()
1037    ///     .unwrap_err();
1038    /// assert_eq!(err.problems().len(), 2);
1039    /// ```
1040    pub fn build(self) -> std::result::Result<App<G, E>, BuildError> {
1041        let mut problems = self.early;
1042        let mut problem = |subject: &str, message: String| {
1043            problems.push(Problem {
1044                subject: subject.to_owned(),
1045                message,
1046            });
1047        };
1048
1049        // Site.
1050        let base = self.base.trim_end_matches('/');
1051        if !base.is_empty() {
1052            match url::normalize(base, self.site.lowercase) {
1053                Ok(n) if n == base && !base.contains(['?', '#']) => {}
1054                _ => problem(
1055                    "base",
1056                    format!("'{}' is not a canonical path such as '/blog'", self.base),
1057                ),
1058            }
1059        }
1060        for h in self.site.hosts.iter().chain(&self.site.aliases) {
1061            if !valid_host(h) {
1062                problem("host", format!("'{h}' is not a host name or address"));
1063            }
1064        }
1065        if self.site.any_host && !self.site.hosts.is_empty() {
1066            problem(
1067                "any_host",
1068                "any_host() and host(..) contradict each other; keep one".into(),
1069            );
1070        }
1071        if self.site.https_only && self.site.hosts.is_empty() {
1072            problem(
1073                "https_only",
1074                "redirecting to HTTPS needs a declared host; add .host(\"example.com\")".into(),
1075            );
1076        }
1077        if let Some(s) = &self.sign_in {
1078            if !is_local_target(s) {
1079                problem("sign_in", format!("'{s}' is not a site-relative path"));
1080            }
1081        }
1082        if self.challenge.is_empty() || !self.challenge.bytes().all(|b| (0x20..0x7f).contains(&b)) {
1083            problem("www_authenticate", "the value must be visible ASCII".into());
1084        }
1085
1086        if !self.security.valid() {
1087            problem(
1088                "security_headers",
1089                "every header value must be visible ASCII and not empty".into(),
1090            );
1091        }
1092        let mut assets = self.assets;
1093        for a in &mut assets {
1094            let subject = format!("assets {}", a.mount);
1095            if a.mount.is_empty()
1096                || !is_local_target(&a.mount)
1097                || url::normalize(&a.mount, self.site.lowercase).map_or(true, |n| n != a.mount)
1098            {
1099                problem(
1100                    &subject,
1101                    "the mount must be a canonical path such as '/static'".into(),
1102                );
1103            }
1104            match std::fs::canonicalize(&a.dir) {
1105                Ok(root) if root.is_dir() => a.root = root,
1106                _ => problem(
1107                    &subject,
1108                    format!("'{}' is not a folder that exists", a.dir.display()),
1109                ),
1110            }
1111            for r in &self.routes {
1112                if a.strip(&r.pattern).is_some() {
1113                    problem(
1114                        &r.pattern,
1115                        format!(
1116                            "the route is under the assets mount '{}' and can never run",
1117                            a.mount
1118                        ),
1119                    );
1120                }
1121            }
1122        }
1123        for h in &self.redirect_hosts {
1124            if !valid_host(h) {
1125                problem(
1126                    "redirect_hosts",
1127                    format!("'{h}' is not a host name or address"),
1128                );
1129            }
1130        }
1131        let mut health = Vec::new();
1132        for (p, h) in self.health {
1133            if !is_local_target(&p) || url::normalize(&p, false).map_or(true, |n| n != p) {
1134                problem(
1135                    "health",
1136                    format!("'{p}' is not a canonical site-relative path"),
1137                );
1138            }
1139            health.push((p.into_boxed_str(), h));
1140        }
1141
1142        // Routes.
1143        let mut trie = Trie::default();
1144        let mut endpoints: Vec<Endpoint> = Vec::new();
1145        let mut entries: Vec<Entry<G, E>> = Vec::new();
1146        let mut all = MethodSet::EMPTY;
1147        for r in self.routes {
1148            let parts = match router::parse(&r.pattern) {
1149                Ok(p) => p,
1150                Err(e) => {
1151                    problem(&r.pattern, e.message());
1152                    continue;
1153                }
1154            };
1155            if r.handlers.is_empty() {
1156                problem(&r.pattern, "the route has no handler".into());
1157                continue;
1158            }
1159            // Request paths are lowered before routing, so a literal with an
1160            // upper-case letter could never match.
1161            if self.site.lowercase
1162                && r.pattern
1163                    .split('/')
1164                    .filter(|s| !s.starts_with([':', '*']))
1165                    .any(|s| s.bytes().any(|b| b.is_ascii_uppercase()))
1166            {
1167                problem(
1168                    &r.pattern,
1169                    "with lowercase_paths, request paths are lowered, so a literal must be lower case to match".into(),
1170                );
1171                continue;
1172            }
1173            let mut access = r.access;
1174            if matches!(access, Access::Undeclared) && rules_impossible::<G::Rule>() {
1175                // A gate without rules has only public routes.
1176                access = Access::Public;
1177            }
1178            if matches!(access, Access::PublicInRuled) {
1179                problem(
1180                    &r.pattern,
1181                    "the route is public inside a group with a rule; the group's rule holds for every route in it, so move the route out of the group".into(),
1182                );
1183                continue;
1184            }
1185            if matches!(access, Access::Undeclared) {
1186                problem(
1187                    &r.pattern,
1188                    "the route declares no access; add .public() or .allow(rule)".into(),
1189                );
1190                continue;
1191            }
1192            let template = match (r.template, &self.render) {
1193                (None, _) => None,
1194                (Some(t), None) => {
1195                    problem(
1196                        &r.pattern,
1197                        format!("template '{t}' is declared but no render engine is set"),
1198                    );
1199                    None
1200                }
1201                (Some(t), Some(engine)) => {
1202                    let id = engine.template(t);
1203                    if id.is_none() {
1204                        problem(&r.pattern, format!("the engine has no template '{t}'"));
1205                    }
1206                    id
1207                }
1208            };
1209            let ep = trie.insert(&parts, endpoints.len() as u32) as usize;
1210            if ep == endpoints.len() {
1211                endpoints.push(Endpoint {
1212                    methods: [None; 10],
1213                    allow: Box::from(""),
1214                    set: MethodSet::EMPTY,
1215                });
1216            }
1217            let names: Box<[Box<str>]> = router::names(&parts).into();
1218            if r.csrf_exempt.is_some_and(|reason| reason.trim().is_empty()) {
1219                problem(&r.pattern, "a CSRF exemption needs its reason".into());
1220            }
1221            if let Some(why) = r.cors.as_deref().and_then(Cors::problem) {
1222                problem(&r.pattern, why.into());
1223            }
1224            if let Err(e) = r.form_limits.validate() {
1225                problem(&r.pattern, format!("the form limits are inconsistent: {e}"));
1226            }
1227            for (m, h) in r.handlers {
1228                let subject = format!("{} {}", m.as_str().unwrap_or("?"), r.pattern);
1229                if m == Method::Other || m == Method::Connect || m == Method::Trace {
1230                    problem(&subject, "this method cannot be routed".into());
1231                    continue;
1232                }
1233                let slot = &mut endpoints[ep].methods[m.index() as usize];
1234                if slot.is_some() {
1235                    problem(
1236                        &subject,
1237                        "a handler for this method and path exists already".into(),
1238                    );
1239                    continue;
1240                }
1241                *slot = Some(entries.len() as u32);
1242                let csrf = r.csrf_exempt.is_none() && !m.is_safe();
1243                if csrf && self.csrf.is_none() {
1244                    problem(
1245                        &subject,
1246                        "an unsafe method needs CSRF protection; set Builder::csrf(..) or mark the route csrf_exempt(\"reason\")".into(),
1247                    );
1248                }
1249                let pick = |at: Hook| -> Box<[HookFn<G::Identity, E>]> {
1250                    self.hooks
1251                        .iter()
1252                        .filter(|(h, sel, _)| *h == at && sel.matches(&r.pattern, r.name))
1253                        .map(|(_, _, f)| Arc::clone(f))
1254                        .collect()
1255                };
1256                entries.push(Entry {
1257                    handler: h,
1258                    access: access.clone(),
1259                    names: names.clone(),
1260                    pattern: r.pattern.as_str().into(),
1261                    work: r.work,
1262                    template,
1263                    csrf,
1264                    limits: r.form_limits,
1265                    cors: r.cors.clone(),
1266                    cache: r.cache,
1267                    cache_values: (
1268                        r.cache.value(true).map(String::into_boxed_str),
1269                        r.cache.value(false).map(String::into_boxed_str),
1270                    ),
1271                    routed: pick(Hook::Routed),
1272                    finish: pick(Hook::Finish),
1273                    before_render: pick(Hook::BeforeRender),
1274                });
1275            }
1276        }
1277        for e in &mut endpoints {
1278            let mut set = MethodSet::EMPTY;
1279            for m in [
1280                Method::Get,
1281                Method::Head,
1282                Method::Post,
1283                Method::Put,
1284                Method::Delete,
1285                Method::Options,
1286                Method::Patch,
1287            ] {
1288                if e.methods[m.index() as usize].is_some() {
1289                    set = set.with(m);
1290                }
1291            }
1292            if set.contains(Method::Get) {
1293                set = set.with(Method::Head);
1294            }
1295            set = set.with(Method::Options);
1296            e.set = set;
1297            all = all.union(set);
1298            let mut v = Vec::new();
1299            set.write_allow(&mut v);
1300            e.allow = String::from_utf8(v).unwrap_or_default().into();
1301        }
1302        if !problems.is_empty() {
1303            return Err(BuildError::new(problems));
1304        }
1305        let mut v = Vec::new();
1306        all.with(Method::Options).write_allow(&mut v);
1307        Ok(App {
1308            gate: self.gate,
1309            shared: Shared {
1310                render: self.render,
1311                checkpoint: self.checkpoint,
1312                events: self.events,
1313                page_limit: self.page_limit,
1314                base: base.into(),
1315                sign_in: self.sign_in.map(Into::into),
1316                challenge: self.challenge.into(),
1317                wipe: self.wipe,
1318                security: self.security,
1319                csrf: self.csrf,
1320                redirect_hosts: self.redirect_hosts,
1321                compression: self.compression,
1322            },
1323            site: self.site,
1324            trie,
1325            endpoints,
1326            entries,
1327            allow_all: String::from_utf8(v).unwrap_or_default().into(),
1328            assets,
1329            health,
1330            streams: assets::Streams::default(),
1331        })
1332    }
1333}
1334
1335impl<G: Gate> Builder<G, NoRender> {
1336    /// The template engine. Set it before adding routes and hooks: their
1337    /// handlers are typed by it, so any added before are dropped and the
1338    /// build says so.
1339    ///
1340    /// ```
1341    /// use web_net::{App, Context, Guest, Output, Render, RenderError, TemplateId, get};
1342    ///
1343    /// struct Pages;
1344    /// impl Render for Pages {
1345    ///     type View<'a> = str;
1346    ///     fn template(&self, name: &str) -> Option<TemplateId> {
1347    ///         (name == "page").then_some(TemplateId(0))
1348    ///     }
1349    ///     fn render(&self, _: TemplateId, title: &str, out: &mut Output<'_>) -> Result<(), RenderError> {
1350    ///         out.write_str("<h1>")?;
1351    ///         out.write_str(title)?; // a real engine escapes
1352    ///         out.write_str("</h1>")
1353    ///     }
1354    /// }
1355    ///
1356    /// let app = App::builder()
1357    ///     .render(Pages)
1358    ///     .route(get("/", |cx: &mut Context<'_, Guest, Pages>| cx.render("Home")).template("page"))
1359    ///     .build()?;
1360    /// let r = serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n");
1361    /// assert_eq!(r.text(), "<h1>Home</h1>");
1362    /// # Ok::<(), web_net::BuildError>(())
1363    /// ```
1364    #[must_use]
1365    pub fn render<E: Render>(self, engine: E) -> Builder<G, E> {
1366        let mut b = Builder::new(self.gate);
1367        if !self.routes.is_empty() || !self.hooks.is_empty() {
1368            b.early.push(Problem {
1369                subject: "render".into(),
1370                message: "set the template engine before adding routes and hooks".into(),
1371            });
1372        }
1373        b.render = Some(engine);
1374        b.site = self.site;
1375        b.base = self.base;
1376        b.checkpoint = self.checkpoint;
1377        b.events = self.events;
1378        b.page_limit = self.page_limit;
1379        b.sign_in = self.sign_in;
1380        b.challenge = self.challenge;
1381        b.wipe = self.wipe;
1382        b.security = self.security;
1383        b.assets = self.assets;
1384        b.health = self.health;
1385        b.csrf = self.csrf;
1386        b.redirect_hosts = self.redirect_hosts;
1387        b.compression = self.compression;
1388        b
1389    }
1390}
1391
1392impl<G: Gate, E: Render> fmt::Debug for Builder<G, E> {
1393    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1394        f.debug_struct("Builder")
1395            .field("routes", &self.routes)
1396            .field("site", &self.site)
1397            .field("base", &self.base)
1398            .finish_non_exhaustive()
1399    }
1400}
1401
1402/// Whether the gate's rule type is [`NoRule`](crate::NoRule), which has no
1403/// values: no route can be anything but public.
1404fn rules_impossible<R: 'static>() -> bool {
1405    std::any::TypeId::of::<R>() == std::any::TypeId::of::<crate::NoRule>()
1406}
1407
1408fn valid_host(h: &str) -> bool {
1409    let (host, port) = split_port(h);
1410    let host_ok = if let Some(inner) = host.strip_prefix('[').and_then(|r| r.strip_suffix(']')) {
1411        inner.parse::<std::net::Ipv6Addr>().is_ok()
1412    } else {
1413        !host.is_empty()
1414            && host.len() <= 253
1415            && host
1416                .bytes()
1417                .all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'.')
1418    };
1419    host_ok && port.is_none_or(|p| !p.is_empty() && p.parse::<u16>().is_ok())
1420}
1421
1422/// `example.com:8080` to (`example.com`, `Some("8080")`); `[::1]:80` to
1423/// (`[::1]`, `Some("80")`).
1424fn split_port(a: &str) -> (&str, Option<&str>) {
1425    if a.starts_with('[') {
1426        return match a.find(']') {
1427            Some(i) => match a[i + 1..].strip_prefix(':') {
1428                Some(p) => (&a[..=i], Some(p)),
1429                None => (a, None),
1430            },
1431            None => (a, None),
1432        };
1433    }
1434    match a.rsplit_once(':') {
1435        Some((h, p)) => (h, Some(p)),
1436        None => (a, None),
1437    }
1438}
1439
1440/// Case-insensitive, so no request needs a lower-case copy of its host.
1441fn is_dev_host(host: &str) -> bool {
1442    const SUFFIX: &[u8] = b".localhost";
1443    let b = host.as_bytes();
1444    ["localhost", "127.0.0.1", "[::1]"]
1445        .iter()
1446        .any(|d| d.eq_ignore_ascii_case(host))
1447        || (b.len() > SUFFIX.len() && b[b.len() - SUFFIX.len()..].eq_ignore_ascii_case(SUFFIX))
1448}
1449
1450/// The authority without trailing dots, and its host without the port.
1451fn host_parts(authority: &str) -> (&str, &str) {
1452    let full = authority.trim_end_matches('.');
1453    (full, split_port(full).0.trim_end_matches('.'))
1454}
1455
1456/// Whether a declared host (lower case) names this authority: the whole
1457/// authority, or the host when the declared one has no port.
1458fn host_listed(list: &[Box<str>], full: &str, host: &str) -> bool {
1459    list.iter().any(|h| {
1460        h.eq_ignore_ascii_case(full) || (split_port(h).1.is_none() && h.eq_ignore_ascii_case(host))
1461    })
1462}
1463
1464thread_local! {
1465    /// Reply buffers reused between requests on one thread. Taken for the
1466    /// length of one request and put back, so nothing about a request lives
1467    /// here between calls.
1468    static SCRATCH: Cell<Option<Box<Out>>> = const { Cell::new(None) };
1469}
1470
1471/// Buffers bigger than this are dropped instead of kept.
1472const KEEP_CAPACITY: usize = 1 << 20;
1473
1474/// Where the site check sent a request.
1475enum Placed<'a> {
1476    Here(Cow<'a, str>),
1477    Redirect(String),
1478    Refuse(u16, &'static str, Option<url::UrlError>),
1479}
1480
1481impl<G: Gate, E: Render> App<G, E> {
1482    /// Serves the application on `addr` (`"127.0.0.1:8080"`): one TCP
1483    /// listener per address it resolves to, with serve-net's default
1484    /// limits, keep-alive, and workers. For TLS, several listeners, or
1485    /// other limits, hand the app to `serve_net::Server::builder()...serve(app)`.
1486    ///
1487    /// ```no_run
1488    /// use web_net::{App, get};
1489    ///
1490    /// App::builder()
1491    ///     .route(get("/", |cx| cx.html(200, "<h1>Hello</h1>")))
1492    ///     .build()?
1493    ///     .listen("127.0.0.1:8080")?
1494    ///     .wait();
1495    /// # Ok::<(), Box<dyn std::error::Error>>(())
1496    /// ```
1497    ///
1498    /// # Errors
1499    ///
1500    /// serve-net's [`ServeError`](serve_net::ServeError): the address does
1501    /// not resolve, or a port cannot be bound.
1502    pub fn listen(
1503        self,
1504        addr: impl std::net::ToSocketAddrs,
1505    ) -> std::result::Result<serve_net::Running, serve_net::ServeError> {
1506        listen(addr, self)
1507    }
1508
1509    /// Runs the pipeline for one request, for a wrapper that holds the app
1510    /// (a gateway). `who` is `Some` when the wrapper authenticated the
1511    /// request already: web-net takes it as given and does not call the
1512    /// gate's `authenticate` (nor write the gate's cookie). Authorization
1513    /// still runs for every route that is not public. `None` runs the whole
1514    /// pipeline, as serve-net's `HttpService::call` does.
1515    ///
1516    /// The report says what ran and whether a cache in front may keep the
1517    /// reply for guests.
1518    ///
1519    /// A wrapper forwards serve-net's `HttpService::done` to the app as
1520    /// well (`HttpService::done(&app, env, finished)`): a large file sent in
1521    /// parts keeps a record until its last part or until `done`, and records
1522    /// of downloads the client dropped are freed only there. Without it they
1523    /// accumulate until new large-file replies are refused with 503.
1524    ///
1525    /// ```
1526    /// use serve_net::{Envelope, HttpReply, HttpRequest, HttpService, Outcome};
1527    /// use web_net::{App, Guest, get};
1528    ///
1529    /// // A front that authenticated every request already.
1530    /// struct Front(App);
1531    /// impl HttpService for Front {
1532    ///     fn call(&self, req: &HttpRequest<'_>, env: &Envelope, reply: &mut HttpReply) -> Outcome {
1533    ///         self.0.respond(req, env, Some(&Guest), reply).outcome
1534    ///     }
1535    /// }
1536    ///
1537    /// let front = Front(App::builder().route(get("/", |cx| cx.text(200, "hi"))).build()?);
1538    /// let r = serve_net::testing::respond(&front, b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n");
1539    /// assert_eq!(r.text(), "hi");
1540    /// # Ok::<(), web_net::BuildError>(())
1541    /// ```
1542    pub fn respond(
1543        &self,
1544        req: &HttpRequest<'_>,
1545        env: &Envelope,
1546        who: Option<&G::Identity>,
1547        reply: &mut HttpReply,
1548    ) -> Served {
1549        let mut report = Served {
1550            outcome: Outcome::Done,
1551            route: None,
1552            cache: Cacheable::No,
1553        };
1554        // Handlers and hooks are caught where they run; this catches the
1555        // rest of the application's code on the request path (the gate, a
1556        // checkpoint, a readiness check, identity methods). Whatever the
1557        // reply held is discarded and the built-in 500 goes out instead.
1558        match catch_unwind(AssertUnwindSafe(|| {
1559            self.run(req, env, who, reply, &mut report)
1560        })) {
1561            Ok(outcome) => report.outcome = outcome,
1562            Err(_) => {
1563                *reply.parts_mut() = Default::default();
1564                report = Served {
1565                    outcome: Outcome::Done,
1566                    route: None,
1567                    cache: Cacheable::No,
1568                };
1569                let at = At {
1570                    env,
1571                    tls: self.tls(env, req.head().authority()),
1572                    json: wants_json(req.header("accept")),
1573                };
1574                self.refuse_with(
1575                    500,
1576                    "pipeline.panic",
1577                    EventKind::Panicked,
1578                    None,
1579                    None,
1580                    at,
1581                    reply,
1582                    false,
1583                );
1584            }
1585        }
1586        report
1587    }
1588
1589    /// Looks at a head before admission: which route would run and how it
1590    /// behaves. Nothing is answered and no gate is called.
1591    ///
1592    /// ```
1593    /// use serve_net::WorkClass;
1594    /// use web_net::{App, get};
1595    /// use serve_net::{Envelope, HttpReply, HttpRequest, HttpService, Outcome};
1596    ///
1597    /// // A dispatcher in front of the app that answers with what `classify` said.
1598    /// struct Probe(App);
1599    /// impl HttpService for Probe {
1600    ///     fn call(&self, req: &HttpRequest<'_>, _: &Envelope, reply: &mut HttpReply) -> Outcome {
1601    ///         let c = self.0.classify(req.head());
1602    ///         reply.text(200, &format!("{:?} {:?}", c.route, c.work));
1603    ///         Outcome::Done
1604    ///     }
1605    /// }
1606    ///
1607    /// let app = App::builder()
1608    ///     .route(get("/report", |cx| cx.text(200, "report")).work(WorkClass::Io))
1609    ///     .build()?;
1610    /// let r = serve_net::testing::respond(&Probe(app), b"GET /report HTTP/1.1\r\nHost: localhost\r\n\r\n");
1611    /// assert_eq!(r.text(), "Some(RouteId(0)) Io");
1612    /// # Ok::<(), web_net::BuildError>(())
1613    /// ```
1614    #[must_use]
1615    pub fn classify(&self, head: &HttpHead<'_>) -> Classified {
1616        let none = Classified {
1617            route: None,
1618            work: WorkClass::Quick,
1619        };
1620        let Placed::Here(path) = self.place(head.authority(), head.path(), Scheme::Https, true)
1621        else {
1622            return none;
1623        };
1624        let Some((ep, _)) = self.trie.find(&path) else {
1625            return none;
1626        };
1627        match self.entry_for(ep as usize, head.method()) {
1628            Some(i) => Classified {
1629                route: Some(RouteId(i)),
1630                work: self.entries[i as usize].work,
1631            },
1632            None => none,
1633        }
1634    }
1635
1636    /// `Ok` when `who` may use at least one method of the endpoint (one is
1637    /// public, or every rule of one allows); else the first denial.
1638    fn may_use_any<'r>(
1639        &'r self,
1640        endpoint: &Endpoint,
1641        who: &G::Identity,
1642        req: &mut Request<'r>,
1643    ) -> std::result::Result<(), Denial> {
1644        let mut first = None;
1645        for i in endpoint.methods.iter().flatten() {
1646            let entry = &self.entries[*i as usize];
1647            let rules = match &entry.access {
1648                Access::Rules(rules) => rules,
1649                _ => return Ok(()),
1650            };
1651            req.route = Some(&entry.pattern);
1652            match rules
1653                .iter()
1654                .try_for_each(|r| self.gate.authorize(who, r, req))
1655            {
1656                Ok(()) => return Ok(()),
1657                Err(d) => {
1658                    first.get_or_insert(d);
1659                }
1660            }
1661        }
1662        first.map_or(Ok(()), Err)
1663    }
1664
1665    /// What a request held once it is over, for `HttpService::done`.
1666    pub(crate) fn forget(&self, env: &Envelope) {
1667        self.streams
1668            .remove((env.connection().get(), env.seq().get()));
1669    }
1670
1671    /// The gate.
1672    ///
1673    /// ```
1674    /// use web_net::{App, Open};
1675    ///
1676    /// let app = App::builder().build()?;
1677    /// let gate: &Open = app.gate();
1678    /// # let _ = gate;
1679    /// # Ok::<(), web_net::BuildError>(())
1680    /// ```
1681    #[must_use]
1682    pub fn gate(&self) -> &G {
1683        &self.gate
1684    }
1685
1686    fn entry_for(&self, ep: usize, m: Method) -> Option<u32> {
1687        let methods = &self.endpoints[ep].methods;
1688        match m {
1689            Method::Other => None,
1690            Method::Head => {
1691                methods[Method::Head.index() as usize].or(methods[Method::Get.index() as usize])
1692            }
1693            m => methods[m.index() as usize],
1694        }
1695    }
1696
1697    /// Steps 1 and 2: which site, which canonical path. `quiet` (for
1698    /// `classify`) skips building a redirect.
1699    fn place<'a>(
1700        &self,
1701        authority: Option<&'a str>,
1702        raw: &'a str,
1703        scheme: Scheme,
1704        quiet: bool,
1705    ) -> Placed<'a> {
1706        let site = &self.site;
1707        let authority = authority.unwrap_or("");
1708        let (full, host_only) = host_parts(authority);
1709        let listed = |list: &[Box<str>]| host_listed(list, full, host_only);
1710        // With no host declared, the development hosts are the site, and no
1711        // other: a name rebound to this address by someone else's DNS is
1712        // not (DNS rebinding), unless the site said `any_host`.
1713        let dev_only = site.hosts.is_empty() && !site.any_host;
1714        let dev = (site.development || dev_only) && is_dev_host(host_only);
1715        if dev_only && !dev {
1716            return Placed::Refuse(421, "host.unknown", None);
1717        }
1718        let mut target_host: Option<&str> = None;
1719        // With hosts declared, an empty or missing Host is as unknown as any
1720        // other host.
1721        if !site.hosts.is_empty() && !dev && !listed(&site.hosts) {
1722            if listed(&site.aliases) {
1723                target_host = site.hosts.first().map(AsRef::as_ref);
1724            } else {
1725                return Placed::Refuse(421, "host.unknown", None);
1726            }
1727        }
1728        let mut https = scheme == Scheme::Https;
1729        if site.https_only && !https && !dev {
1730            https = true;
1731            target_host = target_host.or(site.hosts.first().map(AsRef::as_ref));
1732        }
1733
1734        // The path: canonical, inside the base, with the slash policy.
1735        let canon = match url::normalize(raw, site.lowercase) {
1736            Ok(c) => c,
1737            Err(e) => return Placed::Refuse(400, e.code(), Some(e)),
1738        };
1739        let base = self.shared.base.as_ref();
1740        let c: &str = &canon;
1741        let rel_start = if base.is_empty() {
1742            0
1743        } else if c == base {
1744            c.len()
1745        } else if c.strip_prefix(base).is_some_and(|r| r.starts_with('/')) {
1746            base.len()
1747        } else {
1748            return Placed::Refuse(404, "site.outside_base", None);
1749        };
1750        let rel = &c[rel_start..];
1751        let mut fixed: Option<String> = None;
1752        let health = self.health.iter().any(|(p, _)| p.as_ref() == rel);
1753        if rel.is_empty() {
1754            fixed = Some("/".into());
1755        } else if rel.len() > 1 && !health {
1756            match site.slash {
1757                TrailingSlash::Strip => {
1758                    if let Some(s) = rel.strip_suffix('/') {
1759                        fixed = Some(s.to_owned());
1760                    }
1761                }
1762                TrailingSlash::Add => {
1763                    let trimmed = rel.strip_suffix('/').unwrap_or(rel);
1764                    let file_like = trimmed.rsplit('/').next().unwrap_or("").contains('.');
1765                    if file_like && rel.ends_with('/') {
1766                        // `/style.css/` and `/style.css` are one page: no slash.
1767                        fixed = Some(trimmed.to_owned());
1768                    } else if !file_like && !rel.ends_with('/') {
1769                        fixed = Some(format!("{rel}/"));
1770                    }
1771                }
1772                TrailingSlash::Ignore => {}
1773            }
1774        }
1775        if target_host.is_none() && fixed.is_none() {
1776            if let Cow::Borrowed(c) = canon {
1777                let mut path = &c[rel_start..];
1778                if site.slash == TrailingSlash::Ignore && path.len() > 1 {
1779                    path = path.strip_suffix('/').unwrap_or(path);
1780                }
1781                return Placed::Here(Cow::Borrowed(path));
1782            }
1783        }
1784        if quiet {
1785            return Placed::Refuse(0, "", None);
1786        }
1787        let rel_out = fixed.as_deref().unwrap_or(rel);
1788        let mut loc = String::with_capacity(raw.len() + 32);
1789        if let Some(h) = target_host {
1790            loc.push_str(if https { "https://" } else { "http://" });
1791            loc.push_str(h);
1792        }
1793        loc.push_str(base);
1794        loc.push_str(rel_out);
1795        Placed::Redirect(loc)
1796    }
1797
1798    fn run(
1799        &self,
1800        http: &HttpRequest<'_>,
1801        env: &Envelope,
1802        who: Option<&G::Identity>,
1803        reply: &mut HttpReply,
1804        report: &mut Served,
1805    ) -> Outcome {
1806        let method = http.method();
1807        let raw_path = http.path();
1808        let authority = http.head().authority();
1809        let at = At {
1810            env,
1811            tls: self.tls(env, authority),
1812            json: wants_json(http.header("accept")),
1813        };
1814        if raw_path == "*" {
1815            // The host is checked as for any request: `OPTIONS *` to a host
1816            // the site does not serve is a 421 too.
1817            if self.claim(authority, &self.shared.base).is_none() {
1818                self.refuse(
1819                    421,
1820                    "host.unknown",
1821                    EventKind::Refused,
1822                    None,
1823                    None,
1824                    at,
1825                    reply,
1826                );
1827                return Outcome::Done;
1828            }
1829            if method == Method::Options {
1830                reply.status(204).header("allow", self.allow_all.as_bytes());
1831                self.secure(reply, at.tls, |_| false);
1832            } else {
1833                self.refuse(
1834                    400,
1835                    "url.asterisk",
1836                    EventKind::Refused,
1837                    None,
1838                    None,
1839                    at,
1840                    reply,
1841                );
1842            }
1843            return Outcome::Done;
1844        }
1845        let host_raw = authority.unwrap_or("");
1846        let placed = self.place(authority, raw_path, env.client().scheme(), false);
1847        let path = match placed {
1848            Placed::Here(path) => path,
1849            Placed::Redirect(mut loc) => {
1850                if let Some(q) = http.query() {
1851                    loc.push('?');
1852                    loc.push_str(q);
1853                }
1854                let status = if matches!(method, Method::Get | Method::Head) {
1855                    301
1856                } else {
1857                    308
1858                };
1859                reply.status(status).header("location", loc.as_bytes());
1860                self.secure(reply, at.tls, |_| false);
1861                return Outcome::Done;
1862            }
1863            Placed::Refuse(status, code, src) => {
1864                let src = src
1865                    .as_ref()
1866                    .map(|e| e as &(dyn std::error::Error + 'static));
1867                self.refuse(status, code, EventKind::Refused, None, src, at, reply);
1868                return Outcome::Done;
1869            }
1870        };
1871        // Early: health paths and files, before the gate and the router.
1872        if let Some(outcome) = self.early(http, at, &path, reply) {
1873            return outcome;
1874        }
1875        // CORS preflights carry no credentials: answered before the gate.
1876        if let Some(outcome) = self.preflight(http, at, &path, reply) {
1877            return outcome;
1878        }
1879
1880        let req = Request {
1881            http,
1882            env,
1883            path: &path,
1884            host: host_raw,
1885            route: None,
1886            tls: at.tls,
1887            json: at.json,
1888        };
1889
1890        // Authentication, unless the caller did it.
1891        let owned;
1892        let owned_here = who.is_none();
1893        let who: &G::Identity = match who {
1894            Some(w) => w,
1895            None => match self.gate.authenticate(&req) {
1896                Ok(w) => {
1897                    owned = w;
1898                    &owned
1899                }
1900                Err(d) => {
1901                    self.deny(d, &req, reply);
1902                    return Outcome::Done;
1903                }
1904            },
1905        };
1906
1907        let mut gate_cookie = if owned_here { who.set_cookie() } else { None };
1908        let outcome = self.routed(http, env, at, req, who, &mut gate_cookie, reply, report);
1909        // The gate's own cookie (a dead session cleared, a pre-session id
1910        // set) leaves on every exit after authentication, once. A handler's
1911        // reply wrote it already, before the handler's own fields (so a
1912        // fresh session cookie set by the handler wins); every other exit
1913        // writes it here. A caller that authenticated itself writes its own.
1914        if let Some(c) = gate_cookie {
1915            reply.header("set-cookie", c.as_bytes());
1916            report.cache = Cacheable::No;
1917        }
1918        outcome
1919    }
1920
1921    /// Steps 4 onward: route, method, authorization, CSRF, the handler.
1922    #[allow(clippy::too_many_arguments)]
1923    fn routed<'r>(
1924        &'r self,
1925        http: &HttpRequest<'_>,
1926        env: &Envelope,
1927        at: At<'_>,
1928        mut req: Request<'r>,
1929        who: &G::Identity,
1930        gate_cookie: &mut Option<&str>,
1931        reply: &mut HttpReply,
1932        report: &mut Served,
1933    ) -> Outcome {
1934        let method = http.method();
1935        let path = req.path;
1936        let host_raw = req.host;
1937        // Route and method.
1938        let Some((ep, caps)) = self.trie.find(path) else {
1939            self.refuse(404, "route.none", EventKind::Refused, None, None, at, reply);
1940            return Outcome::Done;
1941        };
1942        let endpoint = &self.endpoints[ep as usize];
1943        let Some(idx) = self.entry_for(ep as usize, method) else {
1944            // The method answers (405, `OPTIONS`) say the path exists, so
1945            // they go only to someone who may use one of its methods: a
1946            // route the gate hides reads as missing for every method.
1947            if let Err(d) = self.may_use_any(endpoint, who, &mut req) {
1948                self.deny(d, &req, reply);
1949                return Outcome::Done;
1950            }
1951            if method == Method::Options {
1952                reply.status(204).header("allow", endpoint.allow.as_bytes());
1953                self.secure(reply, at.tls, |_| false);
1954                return Outcome::Done;
1955            }
1956            let status = if method == Method::Other { 501 } else { 405 };
1957            let code = if status == 501 {
1958                "method.unknown"
1959            } else {
1960                "method.not_allowed"
1961            };
1962            self.refuse(status, code, EventKind::Refused, None, None, at, reply);
1963            if status == 405 {
1964                reply.header("allow", endpoint.allow.as_bytes());
1965            }
1966            return Outcome::Done;
1967        };
1968        let entry = &self.entries[idx as usize];
1969        req.route = Some(&entry.pattern);
1970        report.route = Some(RouteId(idx));
1971
1972        // Authorization: every rule, the groups' first.
1973        if let Access::Rules(rules) = &entry.access {
1974            for rule in rules {
1975                if let Err(d) = self.gate.authorize(who, rule, &req) {
1976                    self.deny(d, &req, reply);
1977                    return Outcome::Done;
1978                }
1979            }
1980        }
1981        if let Some(cp) = &self.shared.checkpoint {
1982            match cp.check(env, Stage::Authorized) {
1983                Verdict::Pass => {}
1984                Verdict::Deny { status } => {
1985                    // A refusal is an error status, whatever the checkpoint
1986                    // said: anything else is answered as 403.
1987                    let status = if (400..=599).contains(&status) {
1988                        status
1989                    } else {
1990                        403
1991                    };
1992                    self.refuse(
1993                        status,
1994                        "checkpoint.denied",
1995                        EventKind::Revoked,
1996                        req.route,
1997                        None,
1998                        at,
1999                        reply,
2000                    );
2001                    return Outcome::Done;
2002                }
2003                _ => {
2004                    self.refuse(
2005                        403,
2006                        "checkpoint.killed",
2007                        EventKind::Revoked,
2008                        req.route,
2009                        None,
2010                        at,
2011                        reply,
2012                    );
2013                    return Outcome::Close;
2014                }
2015            }
2016        }
2017
2018        // CSRF on unsafe methods.
2019        let binding = who.csrf_binding();
2020        match (&self.shared.csrf, entry.csrf) {
2021            (Some(engine), true) => {
2022                let scheme = if env.client().scheme() == Scheme::Https {
2023                    "https://"
2024                } else {
2025                    "http://"
2026                };
2027                let own = format!("{scheme}{}", host_raw.to_ascii_lowercase());
2028                match csrf::check(engine, http, binding, &own, &entry.limits) {
2029                    Ok(()) => {}
2030                    Err(code) => {
2031                        self.refuse(403, code, EventKind::Denied, req.route, None, at, reply);
2032                        return Outcome::Done;
2033                    }
2034                }
2035            }
2036            // Built only with an engine when a route needs one.
2037            (None, true) => {
2038                self.refuse(
2039                    500,
2040                    "csrf.no_engine",
2041                    EventKind::Failed,
2042                    req.route,
2043                    None,
2044                    at,
2045                    reply,
2046                );
2047                return Outcome::Done;
2048            }
2049            (_, false) => {}
2050        }
2051
2052        // The handler, with its hooks, on a buffer web-net holds.
2053        let mut out = SCRATCH.with(Cell::take).unwrap_or_default();
2054        out.reset();
2055        let outcome = self.handle(
2056            entry,
2057            req,
2058            who,
2059            caps,
2060            (gate_cookie, binding),
2061            &mut out,
2062            reply,
2063            &mut report.cache,
2064        );
2065        if self.shared.wipe {
2066            out.wipe();
2067        }
2068        if out.body.capacity() <= KEEP_CAPACITY && out.values.capacity() <= KEEP_CAPACITY {
2069            SCRATCH.with(|s| s.set(Some(out)));
2070        }
2071        outcome
2072    }
2073
2074    /// A CORS preflight for a route with a policy: answered before the gate.
2075    fn preflight(
2076        &self,
2077        http: &HttpRequest<'_>,
2078        at: At<'_>,
2079        path: &str,
2080        reply: &mut HttpReply,
2081    ) -> Option<Outcome> {
2082        if http.method() != Method::Options {
2083            return None;
2084        }
2085        let origin = http.header("origin")?;
2086        let wanted = Method::from_bytes(http.header("access-control-request-method")?);
2087        let (ep, _) = self.trie.find(path)?;
2088        let ep = ep as usize;
2089        if self.entry_for(ep, Method::Options).is_some() {
2090            return None;
2091        }
2092        let cors = self
2093            .entry_for(ep, wanted)
2094            .and_then(|i| self.entries[i as usize].cors.as_ref())?;
2095        // Only the methods whose routes share this policy.
2096        let mut set = MethodSet::EMPTY;
2097        for m in self.endpoints[ep].set.iter() {
2098            let shares = self.entry_for(ep, m).is_some_and(|i| {
2099                self.entries[i as usize]
2100                    .cors
2101                    .as_ref()
2102                    .is_some_and(|c| Arc::ptr_eq(c, cors))
2103            });
2104            if shares {
2105                set = set.with(m);
2106            }
2107        }
2108        let ok = cors.preflight(
2109            origin,
2110            wanted,
2111            http.header("access-control-request-headers"),
2112            set,
2113            reply,
2114        );
2115        if !ok {
2116            self.event(EventKind::Refused, at.env, None, 204, "cors.denied", None);
2117        }
2118        self.secure(reply, at.tls, |_| false);
2119        Some(Outcome::Done)
2120    }
2121
2122    /// Health paths and static files.
2123    fn early(
2124        &self,
2125        http: &HttpRequest<'_>,
2126        at: At<'_>,
2127        path: &str,
2128        reply: &mut HttpReply,
2129    ) -> Option<Outcome> {
2130        let method = http.method();
2131        let get_like = matches!(method, Method::Get | Method::Head);
2132        if let Some((_, h)) = self.health.iter().find(|(p, _)| p.as_ref() == path) {
2133            if !get_like {
2134                self.not_get(method, at, reply);
2135                return Some(Outcome::Done);
2136            }
2137            let (status, text) = match h {
2138                Health::Live => (200, "ok"),
2139                Health::Ready(f) if f() => (200, "ready"),
2140                Health::Ready(_) => (503, "not ready"),
2141            };
2142            reply.text(status, text).header("cache-control", "no-store");
2143            self.secure(reply, at.tls, |_| false);
2144            return Some(Outcome::Done);
2145        }
2146        let (assets, rel) = self
2147            .assets
2148            .iter()
2149            .find_map(|a| a.strip(path).map(|rel| (a, rel)))?;
2150        if !get_like {
2151            self.not_get(method, at, reply);
2152            return Some(Outcome::Done);
2153        }
2154        let key = (at.env.connection().get(), at.env.seq().get());
2155        if let Some(stream) = self.streams.take(key) {
2156            // A later part of a large file: the file found for the first
2157            // part, unchanged, or the end of the connection; never another
2158            // reply inside this one.
2159            return Some(assets::next_part(&self.streams, key, stream, reply));
2160        }
2161        let found = assets::find(assets, rel, http.header("accept-encoding"));
2162        let Some(found) = found else {
2163            self.refuse(
2164                404,
2165                "assets.none",
2166                EventKind::Refused,
2167                None,
2168                None,
2169                at,
2170                reply,
2171            );
2172            return Some(Outcome::Done);
2173        };
2174        match assets::serve(assets, &found, &self.streams, key, http, reply) {
2175            Ok(outcome) => {
2176                self.secure(reply, at.tls, |_| false);
2177                Some(outcome)
2178            }
2179            Err(assets::Unserved::Busy) => {
2180                // Every slot holds a download in progress: refused cleanly,
2181                // never by forgetting one of them.
2182                self.refuse(
2183                    503,
2184                    "assets.busy",
2185                    EventKind::Refused,
2186                    None,
2187                    None,
2188                    at,
2189                    reply,
2190                );
2191                reply.header("retry-after", "5");
2192                Some(Outcome::Done)
2193            }
2194            Err(assets::Unserved::Unreadable) => {
2195                self.refuse(
2196                    500,
2197                    "assets.unreadable",
2198                    EventKind::Failed,
2199                    None,
2200                    None,
2201                    at,
2202                    reply,
2203                );
2204                Some(Outcome::Done)
2205            }
2206        }
2207    }
2208
2209    /// 405 (or 204 for `OPTIONS`) on a path that takes `GET` and `HEAD` only.
2210    fn not_get(&self, method: Method, at: At<'_>, reply: &mut HttpReply) {
2211        if method == Method::Options {
2212            reply.status(204).header("allow", "GET, HEAD, OPTIONS");
2213            self.secure(reply, at.tls, |_| false);
2214        } else {
2215            let (status, code) = if method == Method::Other {
2216                (501, "method.unknown")
2217            } else {
2218                (405, "method.not_allowed")
2219            };
2220            self.refuse(status, code, EventKind::Refused, None, None, at, reply);
2221            if status == 405 {
2222                reply.header("allow", "GET, HEAD, OPTIONS");
2223            }
2224        }
2225    }
2226
2227    #[allow(clippy::too_many_arguments)]
2228    fn handle(
2229        &self,
2230        entry: &Entry<G, E>,
2231        req: Request<'_>,
2232        who: &G::Identity,
2233        caps: Captures,
2234        (gate_cookie, binding): (&mut Option<&str>, Option<&[u8]>),
2235        out: &mut Out,
2236        reply: &mut HttpReply,
2237        cacheable: &mut Cacheable,
2238    ) -> Outcome {
2239        let at = At {
2240            env: req.env,
2241            tls: req.tls,
2242            json: req.json,
2243        };
2244        let route = req.route;
2245        let http = req.http;
2246        let guest = who.is_guest();
2247        let result = {
2248            let mut cx = Context {
2249                req,
2250                who,
2251                caps,
2252                names: &entry.names,
2253                out: &mut *out,
2254                shared: &self.shared,
2255                template: entry.template,
2256                binding,
2257                limits: &entry.limits,
2258                before_render: &entry.before_render,
2259                rendering: false,
2260            };
2261            catch_unwind(AssertUnwindSafe(|| -> Result {
2262                for h in entry.routed.iter() {
2263                    h(&mut cx)?;
2264                }
2265                entry.handler.call(&mut cx)?;
2266                for h in entry.finish.iter() {
2267                    h(&mut cx)?;
2268                }
2269                Ok(())
2270            }))
2271        };
2272        match result {
2273            Ok(Ok(())) if !out.invalid => {
2274                // A handler that sets the gate's cookie itself (a new session
2275                // after sign-in) replaces it: the gate's is not sent.
2276                if gate_cookie.is_some_and(|c| out.sets_cookie(c)) {
2277                    *gate_cookie = None;
2278                }
2279                // A reply that sets a cookie is never public, whatever the
2280                // policy or the handler said: a shared cache would give one
2281                // visitor's cookie (a pre-session id, the CSRF binding) to
2282                // every guest.
2283                let cookies = gate_cookie.is_some() || out.has("set-cookie");
2284                let ours = !out.has("cache-control");
2285                if cookies {
2286                    let public = out
2287                        .get("cache-control")
2288                        .is_some_and(|v| v.windows(6).any(|w| w.eq_ignore_ascii_case(b"public")));
2289                    if ours || public {
2290                        out.remove("cache-control");
2291                        out.header("cache-control", b"private, no-store");
2292                    }
2293                } else if ours {
2294                    let (for_guest, for_member) = &entry.cache_values;
2295                    let value = if guest { for_guest } else { for_member };
2296                    if let Some(v) = value {
2297                        out.header("cache-control", v.as_bytes());
2298                    }
2299                }
2300                if let Some(c) = self.shared.compression {
2301                    compress(c, http.header("accept-encoding"), out);
2302                }
2303                *cacheable = match entry.cache {
2304                    CachePolicy::Guests { max_age } | CachePolicy::Public { max_age }
2305                        if guest
2306                            && ours
2307                            && !cookies
2308                            && entry.cors.is_none()
2309                            && matches!(http.method(), Method::Get | Method::Head)
2310                            && out.status == 200
2311                            && out
2312                                .get("vary")
2313                                .is_none_or(|v| v.eq_ignore_ascii_case(b"accept-encoding")) =>
2314                    {
2315                        Cacheable::Guests { max_age }
2316                    }
2317                    _ => Cacheable::No,
2318                };
2319                reply.status(out.status);
2320                if let Some(c) = gate_cookie.take() {
2321                    reply.header("set-cookie", c.as_bytes());
2322                }
2323                for &(ns, ne, vs, ve) in &out.spans {
2324                    let name = core::str::from_utf8(&out.names[ns as usize..ne as usize])
2325                        .unwrap_or("x-invalid");
2326                    reply.header(name, &out.values[vs as usize..ve as usize]);
2327                }
2328                if !out.has("x-content-type-options") {
2329                    reply.header("x-content-type-options", "nosniff");
2330                }
2331                if let Some(cors) = &entry.cors {
2332                    cors.actual(http.header("origin"), reply);
2333                }
2334                self.secure(reply, at.tls, |n| out.has(n));
2335                reply.body(&out.body);
2336                Outcome::Done
2337            }
2338            Ok(Ok(())) => {
2339                out.reset();
2340                self.refuse(
2341                    500,
2342                    "reply.invalid_field",
2343                    EventKind::Failed,
2344                    route,
2345                    None,
2346                    at,
2347                    reply,
2348                );
2349                Outcome::Done
2350            }
2351            Ok(Err(e)) => {
2352                out.reset();
2353                let src = std::error::Error::source(&e);
2354                self.refuse(
2355                    e.status(),
2356                    e.code(),
2357                    EventKind::Failed,
2358                    route,
2359                    src,
2360                    at,
2361                    reply,
2362                );
2363                Outcome::Done
2364            }
2365            Err(_) => {
2366                out.reset();
2367                self.refuse(
2368                    500,
2369                    "handler.panic",
2370                    EventKind::Panicked,
2371                    route,
2372                    None,
2373                    at,
2374                    reply,
2375                );
2376                Outcome::Done
2377            }
2378        }
2379    }
2380
2381    /// A gate's denial as a response.
2382    fn deny(&self, d: Denial, req: &Request<'_>, reply: &mut HttpReply) {
2383        let env = req.env;
2384        let at = At {
2385            env,
2386            tls: req.tls,
2387            json: req.json,
2388        };
2389        if d.kind() == DenialKind::SignIn {
2390            if let Some(page) = &self.shared.sign_in {
2391                if matches!(req.method(), Method::Get | Method::Head) {
2392                    let mut loc = String::with_capacity(64);
2393                    loc.push_str(&self.shared.base);
2394                    loc.push_str(page);
2395                    loc.push_str(if page.contains('?') {
2396                        "&next="
2397                    } else {
2398                        "?next="
2399                    });
2400                    let mut next = String::new();
2401                    next.push_str(&self.shared.base);
2402                    next.push_str(req.path);
2403                    if let Some(q) = req.query() {
2404                        next.push('?');
2405                        next.push_str(q);
2406                    }
2407                    encode_component(&next, &mut loc);
2408                    self.event(EventKind::Denied, env, req.route, 303, d.code(), None);
2409                    reply.status(303).header("location", loc.as_bytes());
2410                    self.secure(reply, at.tls, |_| false);
2411                    if let Some(c) = d.cookie() {
2412                        reply.header("set-cookie", c.as_bytes());
2413                    }
2414                    return;
2415                }
2416            }
2417        }
2418        let status = d.status();
2419        self.refuse(
2420            status,
2421            d.code(),
2422            EventKind::Denied,
2423            req.route,
2424            None,
2425            at,
2426            reply,
2427        );
2428        if status == 401 {
2429            reply.header("www-authenticate", self.shared.challenge.as_bytes());
2430        }
2431        if let Some(seconds) = d.retry_seconds().filter(|_| matches!(status, 429 | 503)) {
2432            reply.header("retry-after", seconds.to_string().as_bytes());
2433        }
2434        if let Some(c) = d.cookie() {
2435            reply.header("set-cookie", c.as_bytes());
2436        }
2437    }
2438
2439    fn event(
2440        &self,
2441        kind: EventKind,
2442        env: &Envelope,
2443        route: Option<&str>,
2444        status: u16,
2445        code: &'static str,
2446        source: Option<&(dyn std::error::Error + 'static)>,
2447    ) {
2448        let event = Event {
2449            kind,
2450            id: RequestId::of(env),
2451            route,
2452            status,
2453            code,
2454            source,
2455        };
2456        // A sink that panics loses its event, never the visitor's reply.
2457        let _ = catch_unwind(AssertUnwindSafe(|| self.shared.events.event(&event)));
2458    }
2459
2460    /// The one function every refusal and error goes through: the status,
2461    /// the error template for it or the built-in page, the reference; the
2462    /// code and source to the events only.
2463    #[allow(clippy::too_many_arguments)]
2464    fn refuse(
2465        &self,
2466        status: u16,
2467        code: &'static str,
2468        kind: EventKind,
2469        route: Option<&str>,
2470        source: Option<&(dyn std::error::Error + 'static)>,
2471        at: At<'_>,
2472        reply: &mut HttpReply,
2473    ) {
2474        self.refuse_with(status, code, kind, route, source, at, reply, true);
2475    }
2476
2477    /// [`refuse`](Self::refuse); `engine` false gives the built-in page
2478    /// without asking the engine (after a panic, the engine may be what
2479    /// failed).
2480    #[allow(clippy::too_many_arguments)]
2481    fn refuse_with(
2482        &self,
2483        status: u16,
2484        code: &'static str,
2485        kind: EventKind,
2486        route: Option<&str>,
2487        source: Option<&(dyn std::error::Error + 'static)>,
2488        at: At<'_>,
2489        reply: &mut HttpReply,
2490        engine: bool,
2491    ) {
2492        self.event(kind, at.env, route, status, code, source);
2493        let reference = RequestId::of(at.env).to_string();
2494        let mut body = Vec::with_capacity(512);
2495        if at.json {
2496            error::json_body(status, &reference, &mut body);
2497            reply
2498                .status(status)
2499                .header("content-type", "application/json")
2500                .header("x-content-type-options", "nosniff")
2501                .header("cache-control", "no-store")
2502                .body(&body);
2503            self.secure(reply, at.tls, |_| false);
2504            return;
2505        }
2506        let mut done = false;
2507        if let Some(engine) = self.shared.render.as_ref().filter(|_| engine) {
2508            let page = ErrorPage {
2509                status,
2510                title: error::title(status),
2511                message: error::message(status),
2512                reference: &reference,
2513                base: &self.shared.base,
2514            };
2515            let wrote = catch_unwind(AssertUnwindSafe(|| {
2516                let mut out = Output::new(&mut body, self.shared.page_limit);
2517                let wrote = engine.error_page(&page, &mut out);
2518                matches!(wrote, Ok(true)) && !out.over()
2519            }));
2520            match wrote {
2521                Ok(true) => done = true,
2522                Ok(false) => body.clear(),
2523                Err(_) => {
2524                    body.clear();
2525                    self.event(
2526                        EventKind::Panicked,
2527                        at.env,
2528                        route,
2529                        status,
2530                        "render.error_page_panic",
2531                        None,
2532                    );
2533                }
2534            }
2535        }
2536        if !done {
2537            error::builtin_page(status, &reference, &mut body);
2538        }
2539        reply
2540            .status(status)
2541            .header("content-type", "text/html; charset=utf-8")
2542            .header("x-content-type-options", "nosniff")
2543            .header("cache-control", "no-store")
2544            .body(&body);
2545        self.secure(reply, at.tls, |_| false);
2546    }
2547
2548    /// Adds the policy's security headers, except those `has` says the reply
2549    /// set already.
2550    fn secure(&self, reply: &mut HttpReply, tls: bool, has: impl Fn(&str) -> bool) {
2551        for (name, value) in self.shared.security.fields(tls) {
2552            if !has(name) {
2553                reply.header(name, value.as_bytes());
2554            }
2555        }
2556    }
2557
2558    /// How well this application claims a request, for [`Sites`](crate::Sites):
2559    /// the host's rank (2 listed or an alias, 1 a development host, 0 any
2560    /// host accepted) and the base path's length. `None`: not this site.
2561    pub(crate) fn claim(&self, authority: Option<&str>, path: &str) -> Option<(u8, usize)> {
2562        let (full, host_only) = host_parts(authority.unwrap_or(""));
2563        let listed = |list: &[Box<str>]| host_listed(list, full, host_only);
2564        let rank = if self.site.hosts.is_empty() && self.site.any_host {
2565            0
2566        } else if self.site.hosts.is_empty() {
2567            if is_dev_host(host_only) {
2568                1
2569            } else {
2570                return None;
2571            }
2572        } else if listed(&self.site.hosts) || listed(&self.site.aliases) {
2573            2
2574        } else if self.site.development && is_dev_host(host_only) {
2575            1
2576        } else {
2577            return None;
2578        };
2579        let base = self.shared.base.as_ref();
2580        // A site with lower-case paths claims `/BLOG/x` too: it redirects it
2581        // to `/blog/x` itself, instead of another site answering it.
2582        let head = path.get(..base.len());
2583        let starts = head.is_some_and(|h| {
2584            if self.site.lowercase {
2585                h.eq_ignore_ascii_case(base)
2586            } else {
2587                h == base
2588            }
2589        });
2590        let inside = base.is_empty()
2591            || (starts && matches!(path.as_bytes().get(base.len()), None | Some(b'/')));
2592        inside.then_some((rank, base.len()))
2593    }
2594
2595    /// Whether HTTPS-only headers apply: the request came over HTTPS and is
2596    /// not for a development host.
2597    fn tls(&self, env: &Envelope, authority: Option<&str>) -> bool {
2598        if env.client().scheme() != Scheme::Https {
2599            return false;
2600        }
2601        let (_, host) = host_parts(authority.unwrap_or(""));
2602        !(self.site.development && is_dev_host(host))
2603    }
2604}
2605
2606impl<G: Gate, E: Render> HttpService for App<G, E> {
2607    fn call(&self, request: &HttpRequest<'_>, env: &Envelope, reply: &mut HttpReply) -> Outcome {
2608        self.respond(request, env, None, reply).outcome
2609    }
2610
2611    /// Frees what the request held: the record of a large file going out
2612    /// in parts, when the client left before its end.
2613    fn done(&self, env: &Envelope, _finished: &serve_net::Finished) {
2614        self.forget(env);
2615    }
2616}
2617
2618/// One TCP listener per address `addr` resolves to, serving `service`.
2619pub(crate) fn listen<S: HttpService + 'static>(
2620    addr: impl std::net::ToSocketAddrs,
2621    service: S,
2622) -> std::result::Result<serve_net::Running, serve_net::ServeError> {
2623    let addrs: Vec<std::net::SocketAddr> = addr
2624        .to_socket_addrs()
2625        .map(Iterator::collect)
2626        .unwrap_or_default();
2627    let mut server = serve_net::Server::builder();
2628    if addrs.is_empty() {
2629        // Resolves to nothing: serve-net's validation names the rule.
2630        server = server.listener(serve_net::ListenerSpec::tcp(
2631            "web-0",
2632            &[] as &[std::net::SocketAddr],
2633        ));
2634    }
2635    for (i, a) in addrs.iter().enumerate() {
2636        server = server.listener(serve_net::ListenerSpec::tcp(
2637            &format!("web-{i}"),
2638            std::slice::from_ref(a),
2639        ));
2640    }
2641    server.serve(service)
2642}
2643
2644impl<G: Gate, E: Render> fmt::Debug for App<G, E> {
2645    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2646        f.debug_struct("App")
2647            .field("site", &self.site)
2648            .field("base", &self.shared.base)
2649            .field("routes", &self.entries.len())
2650            .finish_non_exhaustive()
2651    }
2652}
2653
2654/// Whether the client prefers JSON to HTML (no `Accept`: HTML).
2655fn wants_json(accept: Option<&[u8]>) -> bool {
2656    // Most requests never name JSON: skip the parse for them.
2657    accept.is_some_and(|a| a.windows(4).any(|w| w.eq_ignore_ascii_case(b"json")))
2658        && parse_net::best_media(accept, &["text/html", "application/json"]) == Some(1)
2659}
2660
2661/// gzip for a reply worth compressing that the client accepts. Adds `Vary`
2662/// for every compressible reply, compressed or not, so caches keep the two
2663/// apart.
2664fn compress(c: Compression, accept_encoding: Option<&[u8]>, out: &mut Out) {
2665    if out.status != 200 || out.has("content-encoding") {
2666        return;
2667    }
2668    if !out.get("content-type").is_some_and(compressible) {
2669        return;
2670    }
2671    // The handler's own `Vary` keeps its names and gains this one, so a
2672    // cache keyed on them never hands a compressed body to a client that
2673    // did not accept it.
2674    match out.get("vary") {
2675        None => out.header("vary", b"accept-encoding"),
2676        Some(v) => {
2677            let named = v == b"*"
2678                || v.split(|&b| b == b',')
2679                    .any(|t| t.trim_ascii().eq_ignore_ascii_case(b"accept-encoding"));
2680            if !named {
2681                let mut joined = v.to_vec();
2682                joined.extend_from_slice(b", accept-encoding");
2683                out.remove("vary");
2684                out.header("vary", &joined);
2685            }
2686        }
2687    }
2688    let accepts = accept_encoding
2689        .and_then(|v| parse_net::best_encoding(Some(v), &["gzip", "identity"]))
2690        == Some(0);
2691    if !accepts || out.body.len() < c.min_bytes {
2692        return;
2693    }
2694    let level = compress_lib::Level::new(c.level).unwrap_or(compress_lib::Level::DEFAULT);
2695    let packed = compress_lib::gzip::compress_with(&out.body, level);
2696    if packed.len() < out.body.len() {
2697        out.body.clear();
2698        out.body.extend_from_slice(&packed);
2699        out.header("content-encoding", b"gzip");
2700    }
2701}
2702
2703/// Percent-encodes a query component: everything but unreserved and `/`.
2704fn encode_component(s: &str, out: &mut String) {
2705    const HEX: &[u8; 16] = b"0123456789ABCDEF";
2706    for b in s.bytes() {
2707        if b.is_ascii_alphanumeric() || matches!(b, b'-' | b'.' | b'_' | b'~' | b'/') {
2708            out.push(b as char);
2709        } else {
2710            out.push('%');
2711            out.push(HEX[(b >> 4) as usize] as char);
2712            out.push(HEX[(b & 15) as usize] as char);
2713        }
2714    }
2715}
2716
2717#[cfg(test)]
2718mod tests {
2719    use super::*;
2720
2721    #[test]
2722    fn test_split_port() {
2723        assert_eq!(split_port("a.test:80"), ("a.test", Some("80")));
2724        assert_eq!(split_port("a.test"), ("a.test", None));
2725        assert_eq!(split_port("[::1]:8080"), ("[::1]", Some("8080")));
2726        assert_eq!(split_port("[::1]"), ("[::1]", None));
2727    }
2728
2729    #[test]
2730    fn test_valid_host() {
2731        for ok in ["example.com", "localhost:8080", "[::1]", "127.0.0.1:80"] {
2732            assert!(valid_host(ok), "{ok}");
2733        }
2734        for bad in ["", "a b", "a:", "a:x", "[zz]", "a/b"] {
2735            assert!(!valid_host(bad), "{bad}");
2736        }
2737    }
2738
2739    #[test]
2740    fn test_selector() {
2741        assert!(Routes::prefix("/blog").matches("/blog", None));
2742        assert!(Routes::prefix("/blog/").matches("/blog/:slug", None));
2743        assert!(!Routes::prefix("/blog").matches("/blogger", None));
2744        assert!(Routes::named(&["a"]).matches("/x", Some("a")));
2745        assert!(!Routes::named(&["a"]).matches("/x", None));
2746    }
2747}