pub struct Builder<G: Gate, E: Render = NoRender> { /* private fields */ }Expand description
Declares an application. Every mistake is reported by
build at once, before anything listens.
use web_net::{App, get};
let app = App::builder()
.host("example.com")
.development()
.health("/healthz")
.route(get("/", |cx| cx.text(200, "home")))
.build()?;
let r = serve_net::testing::respond(&app, b"GET /healthz HTTP/1.1\r\nHost: localhost\r\n\r\n");
assert_eq!(r.text(), "ok");Implementations§
Source§impl<G: Gate, E: Render> Builder<G, E>
impl<G: Gate, E: Render> Builder<G, E>
Sourcepub fn compress(self, compression: Compression) -> Self
pub fn compress(self, compression: Compression) -> Self
Compress replies with gzip when the client accepts it: HTML, CSS, JavaScript, JSON, XML, SVG, and other text, over a minimum size. Off by default: a proxy in front usually compresses.
A compressed page that carries a secret beside text the visitor chose (a reflected query value) can leak the secret through its compressed size (BREACH). csrf-sec masks its tokens per reply; keep compression off for pages with other secrets.
use web_net::{App, Compression, get};
let app = App::builder()
.compress(Compression::default())
.route(get("/", |cx| cx.text(200, &"hello ".repeat(400))))
.build()?;
let r = serve_net::testing::respond(
&app,
b"GET / HTTP/1.1\r\nHost: localhost\r\nAccept-Encoding: gzip\r\n\r\n",
);
assert_eq!(r.header("content-encoding"), Some(&b"gzip"[..]));Sourcepub fn csrf(self, csrf: Csrf) -> Self
pub fn csrf(self, csrf: Csrf) -> Self
CSRF protection with csrf-sec (re-exported as
web_net::csrf_sec), built from a persisted
32-byte key. Required as soon as one route takes an unsafe method and
is not exempt.
web-net always applies the origin policy and a token, whatever the
Csrf’s Mode; its keys, lifetimes, and allowed origins apply as set.
use web_net::{App, csrf_sec::Csrf, get, post};
let app = App::builder()
.csrf(Csrf::new(*b"a persisted 32-byte secret key!!").unwrap())
.route(get("/", |cx| cx.text(200, "form")))
.route(post("/", |cx| cx.redirect(303, "/")))
.build()?;
// A POST without a token is refused before its handler runs.
let r = serve_net::testing::respond(
&app,
b"POST / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 0\r\n\r\n",
);
assert_eq!(r.status, 403);Sourcepub fn redirect_hosts(self, hosts: &[&str]) -> Self
pub fn redirect_hosts(self, hosts: &[&str]) -> Self
Hosts that Context::redirect_external may send a visitor to
(accounts.example.com). Everything else is refused.
use web_net::{App, get};
let app = App::builder()
.redirect_hosts(&["accounts.example.com"])
.route(get("/login", |cx| cx.redirect_external(302, "https://accounts.example.com/login")))
.build()?;
let r = serve_net::testing::respond(&app, b"GET /login HTTP/1.1\r\nHost: localhost\r\n\r\n");
assert_eq!(r.header("location"), Some(&b"https://accounts.example.com/login"[..]));Sourcepub fn assets(self, assets: Assets) -> Self
pub fn assets(self, assets: Assets) -> Self
Serves a folder of files under a mount path, before routing and without the gate (files are public).
use web_net::{App, Assets};
let dir = std::env::temp_dir().join("web-net-doc-builder-assets");
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("hello.txt"), "hello").unwrap();
let app = App::builder().assets(Assets::new("/static", &dir)).build()?;
let r = serve_net::testing::respond(&app, b"GET /static/hello.txt HTTP/1.1\r\nHost: localhost\r\n\r\n");
assert_eq!((r.status, r.text()), (200, "hello"));Sourcepub fn health(self, path: &str) -> Self
pub fn health(self, path: &str) -> Self
A liveness path (/healthz): 200 ok whenever the process answers.
Answered before routing and without the gate.
let app = web_net::App::builder().health("/healthz").build()?;
let r = serve_net::testing::respond(&app, b"GET /healthz HTTP/1.1\r\nHost: localhost\r\n\r\n");
assert_eq!((r.status, r.text()), (200, "ok"));Sourcepub fn ready(
self,
path: &str,
check: impl Fn() -> bool + Send + Sync + 'static,
) -> Self
pub fn ready( self, path: &str, check: impl Fn() -> bool + Send + Sync + 'static, ) -> Self
A readiness path (/readyz): 200 ready when check says so,
503 not ready otherwise. Keep check to a flag read: it runs on the
request path.
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use serve_net::testing::respond;
let up = Arc::new(AtomicBool::new(false));
let flag = Arc::clone(&up);
let app = web_net::App::builder()
.ready("/readyz", move || flag.load(Ordering::Relaxed))
.build()?;
let req = b"GET /readyz HTTP/1.1\r\nHost: localhost\r\n\r\n";
assert_eq!(respond(&app, req).status, 503);
up.store(true, Ordering::Relaxed);
assert_eq!(respond(&app, req).text(), "ready");Sourcepub fn security_headers(self, policy: SecurityHeaders) -> Self
pub fn security_headers(self, policy: SecurityHeaders) -> Self
The security headers every reply carries (default
SecurityHeaders::default).
use web_net::{App, SecurityHeaders, get};
// A proxy in front sets the security headers, so web-net sends none.
let app = App::builder()
.security_headers(SecurityHeaders::none())
.route(get("/", |cx| cx.html(200, "<p>hi</p>")))
.build()?;
let r = serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n");
assert_eq!(r.header("content-security-policy"), None);Sourcepub fn route(self, route: Route<G::Identity, G::Rule, E>) -> Self
pub fn route(self, route: Route<G::Identity, G::Rule, E>) -> Self
Adds a route.
use web_net::{App, get};
let app = App::builder().route(get("/about", |cx| cx.text(200, "about"))).build()?;
let r = serve_net::testing::respond(&app, b"GET /about HTTP/1.1\r\nHost: localhost\r\n\r\n");
assert_eq!(r.text(), "about");Sourcepub fn group(self, group: Group<G::Identity, G::Rule, E>) -> Self
pub fn group(self, group: Group<G::Identity, G::Rule, E>) -> Self
Adds a group’s routes.
use web_net::{App, Group, get};
let app = App::builder()
.group(Group::at("/admin").route(get("/users", |cx| cx.text(200, "users"))))
.build()?;
let r = serve_net::testing::respond(&app, b"GET /admin/users HTTP/1.1\r\nHost: localhost\r\n\r\n");
assert_eq!(r.text(), "users");Sourcepub fn hook<F>(self, hook: Hook, routes: Routes, f: F) -> Self
pub fn hook<F>(self, hook: Hook, routes: Routes, f: F) -> Self
Runs f at hook for the routes routes selects.
use web_net::{App, Hook, Routes, get};
let app = App::builder()
.hook(Hook::Finish, Routes::all(), |cx| {
cx.set_header("x-served-by", "web-net");
Ok(())
})
.route(get("/", |cx| cx.text(200, "home")))
.build()?;
let r = serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n");
assert_eq!(r.header("x-served-by"), Some(&b"web-net"[..]));Sourcepub fn host(self, host: &str) -> Self
pub fn host(self, host: &str) -> Self
A host the site answers (example.com, or localhost:8080 with its
port). The first one is canonical; any other host gets 421.
With no host declared, the site answers the development hosts only
(localhost, 127.0.0.1, [::1], *.localhost), so a site works
on a laptop as it is and a name someone else points at the server
(DNS rebinding) is never the site. Declare the production host before
deploying, or any_host for a site that must
answer every name.
use web_net::{App, get};
use serve_net::testing::respond;
let app = App::builder().route(get("/", |cx| cx.text(200, "hi"))).build()?;
assert_eq!(respond(&app, b"GET / HTTP/1.1\r\nHost: localhost:8080\r\n\r\n").status, 200);
assert_eq!(respond(&app, b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n").status, 421);
let app = App::builder().host("example.com").route(get("/", |cx| cx.text(200, "hi"))).build()?;
assert_eq!(respond(&app, b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n").status, 200);
assert_eq!(respond(&app, b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n").status, 421);Sourcepub fn alias(self, host: &str) -> Self
pub fn alias(self, host: &str) -> Self
A host that is redirected (301, or 308 for unsafe methods) to the
canonical host (www.example.com to example.com).
use web_net::{App, get};
let app = App::builder()
.host("example.com")
.alias("www.example.com")
.route(get("/", |cx| cx.text(200, "home")))
.build()?;
let r = serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n");
assert_eq!((r.status, r.header("location")), (301, Some(&b"http://example.com/"[..])));Sourcepub fn development(self) -> Self
pub fn development(self) -> Self
A development instance of a site with declared hosts: localhost,
127.0.0.1, [::1], and *.localhost are answered as the site too
(with any port), and HTTPS is not required for them. Never set this
in production. (A site with no declared host answers only these
already.)
use web_net::{App, get};
let app = App::builder()
.host("example.com")
.development()
.route(get("/", |cx| cx.text(200, "home")))
.build()?;
let r = serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: localhost:8080\r\n\r\n");
assert_eq!(r.status, 200);Sourcepub fn any_host(self) -> Self
pub fn any_host(self) -> Self
Answers every Host, for a site behind a front that checked the
host already, or one that is reached by names it cannot list. Not
with host.
use web_net::{App, get};
let app = App::builder().any_host().route(get("/", |cx| cx.text(200, "hi"))).build()?;
let r = serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: anything.example\r\n\r\n");
assert_eq!(r.status, 200);Sourcepub fn base(self, base: &str) -> Self
pub fn base(self, base: &str) -> Self
The path the site is installed under (/blog). It is removed before
routing and added back by Context::url, so routes and links are written
as if the site were at /. Requests outside it get 404.
use serve_net::testing::respond;
use web_net::{App, get};
let app = App::builder()
.base("/blog")
.route(get("/posts", |cx| cx.text(200, "posts")))
.build()?;
assert_eq!(respond(&app, b"GET /blog/posts HTTP/1.1\r\nHost: localhost\r\n\r\n").text(), "posts");
assert_eq!(respond(&app, b"GET /posts HTTP/1.1\r\nHost: localhost\r\n\r\n").status, 404);Sourcepub fn trailing_slash(self, policy: TrailingSlash) -> Self
pub fn trailing_slash(self, policy: TrailingSlash) -> Self
The trailing-slash policy (default TrailingSlash::Strip).
use web_net::{App, TrailingSlash, get};
// Both spellings reach the route and neither is redirected.
let app = App::builder()
.trailing_slash(TrailingSlash::Ignore)
.route(get("/docs", |cx| cx.text(200, "docs")))
.build()?;
let r = serve_net::testing::respond(&app, b"GET /docs/ HTTP/1.1\r\nHost: localhost\r\n\r\n");
assert_eq!((r.status, r.text()), (200, "docs"));Sourcepub fn lowercase_paths(self) -> Self
pub fn lowercase_paths(self) -> Self
Paths are lower case: one with an upper-case letter is redirected.
use web_net::{App, get};
let app = App::builder()
.lowercase_paths()
.route(get("/about", |cx| cx.text(200, "about")))
.build()?;
let r = serve_net::testing::respond(&app, b"GET /About HTTP/1.1\r\nHost: localhost\r\n\r\n");
assert_eq!((r.status, r.header("location")), (301, Some(&b"/about"[..])));Sourcepub fn https_only(self) -> Self
pub fn https_only(self) -> Self
Plain HTTP is redirected to HTTPS on the canonical host (behind a TLS proxy, serve-net must trust the proxy’s scheme). Needs a host.
use web_net::{App, get};
let app = App::builder()
.host("example.com")
.https_only()
.route(get("/", |cx| cx.text(200, "home")))
.build()?;
let r = serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n");
assert_eq!((r.status, r.header("location")), (301, Some(&b"https://example.com/"[..])));Sourcepub fn sign_in(self, path: &str) -> Self
pub fn sign_in(self, path: &str) -> Self
The site-relative sign-in page. A Denial::sign_in on a GET or
HEAD is then answered with a 303 to it, the page asked for passed
as next; without it, or for other methods, the answer is 401.
use web_net::{App, get};
// `Members` answers `Rule::Member` with `Denial::sign_in` for everyone.
let app = App::with_gate(Members)
.sign_in("/login")
.route(get("/login", |cx| cx.text(200, "sign in")).public())
.route(get("/account", |cx| cx.text(200, "account")).allow(Rule::Member))
.build()?;
let r = serve_net::testing::respond(&app, b"GET /account HTTP/1.1\r\nHost: localhost\r\n\r\n");
assert_eq!((r.status, r.header("location")), (303, Some(&b"/login?next=/account"[..])));Sourcepub fn www_authenticate(self, challenge: &str) -> Self
pub fn www_authenticate(self, challenge: &str) -> Self
The WWW-Authenticate value sent with a 401 (default Session).
let app = web_net::App::builder().www_authenticate("Bearer").build()?;Sourcepub fn events(self, events: impl Events) -> Self
pub fn events(self, events: impl Events) -> Self
Where refusals, denials, and failures are reported.
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
use web_net::{App, Error, Event, EventKind, Events, get};
#[derive(Default)]
struct Failures(AtomicU32);
impl Events for Failures {
fn event(&self, e: &Event<'_>) {
if e.kind == EventKind::Failed {
self.0.fetch_add(1, Ordering::Relaxed);
}
}
}
let failures = Arc::new(Failures::default());
let app = App::builder()
.events(Arc::clone(&failures))
.route(get("/", |_cx| Err(Error::unavailable("db.down"))))
.build()?;
serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n");
assert_eq!(failures.0.load(Ordering::Relaxed), 1);Sourcepub fn checkpoint(self, checkpoint: impl Checkpoint) -> Self
pub fn checkpoint(self, checkpoint: impl Checkpoint) -> Self
The in-flight revocation check at web-net’s stages.
use serve_net::{Envelope, Verdict};
use web_net::{App, Checkpoint, Stage, get};
struct Maintenance;
impl Checkpoint for Maintenance {
fn check(&self, _: &Envelope, _: Stage) -> Verdict {
Verdict::Deny { status: 503 }
}
}
let app = App::builder()
.checkpoint(Maintenance)
.route(get("/", |cx| cx.text(200, "hi")))
.build()?;
let r = serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n");
assert_eq!(r.status, 503);Sourcepub fn page_limit(self, bytes: usize) -> Self
pub fn page_limit(self, bytes: usize) -> Self
The budget for one rendered page (default 8 MiB).
use web_net::{App, Context, Guest, get};
// `Pages` is an engine whose one template, "page", writes the view (a `str`).
let app = App::builder()
.render(Pages)
.page_limit(4)
.route(get("/", |cx: &mut Context<'_, Guest, Pages>| cx.render("more than four bytes")).template("page"))
.build()?;
let r = serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n");
assert_eq!(r.status, 500);Sourcepub fn wipe_buffers(self) -> Self
pub fn wipe_buffers(self) -> Self
Zero the reply buffers web-net reuses after every request, so one visitor’s page never lingers in memory another request reuses. Costs one pass over the bytes written.
use web_net::{App, get};
let app = App::builder()
.wipe_buffers()
.route(get("/", |cx| cx.text(200, "a private page")))
.build()?;
let r = serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n");
assert_eq!(r.text(), "a private page");Sourcepub fn build(self) -> Result<App<G, E>, BuildError>
pub fn build(self) -> Result<App<G, E>, BuildError>
Compiles the application.
§Errors
A BuildError listing every problem: a route without declared
access, a malformed pattern, two handlers for one method of one
path, an unknown template, a malformed host or base path.
use web_net::{App, get};
assert!(App::builder().route(get("/", |cx| cx.text(200, "hi"))).build().is_ok());
// Every problem is listed, not only the first.
let err = App::builder()
.base("blog")
.route(get("/a", |cx| cx.text(200, "a")))
.route(get("/a", |cx| cx.text(200, "again")))
.build()
.unwrap_err();
assert_eq!(err.problems().len(), 2);Source§impl<G: Gate> Builder<G, NoRender>
impl<G: Gate> Builder<G, NoRender>
Sourcepub fn render<E: Render>(self, engine: E) -> Builder<G, E>
pub fn render<E: Render>(self, engine: E) -> Builder<G, E>
The template engine. Set it before adding routes and hooks: their handlers are typed by it, so any added before are dropped and the build says so.
use web_net::{App, Context, Guest, Output, Render, RenderError, TemplateId, get};
struct Pages;
impl Render for Pages {
type View<'a> = str;
fn template(&self, name: &str) -> Option<TemplateId> {
(name == "page").then_some(TemplateId(0))
}
fn render(&self, _: TemplateId, title: &str, out: &mut Output<'_>) -> Result<(), RenderError> {
out.write_str("<h1>")?;
out.write_str(title)?; // a real engine escapes
out.write_str("</h1>")
}
}
let app = App::builder()
.render(Pages)
.route(get("/", |cx: &mut Context<'_, Guest, Pages>| cx.render("Home")).template("page"))
.build()?;
let r = serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n");
assert_eq!(r.text(), "<h1>Home</h1>");