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}