web-net 0.10.0

Web layer on serve-net: URL normalization, sites and base paths, a compiled router with 404 and 405, gate checkpoints for authentication and authorization, hooks, error pages, and a render trait that reads typed views.
Documentation

Quick start

[dependencies]
web-net = "0.10"
use web_net::{App, get};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    App::builder()
        .route(get("/", |cx| cx.html(200, "<h1>Hello</h1>")))
        .route(get("/posts/:slug", |cx| cx.text(200, &cx.param("slug")?)))
        .build()?
        .listen("127.0.0.1:8080")?
        .wait();
    Ok(())
}

A handler ends with one reply: cx.html, cx.text, cx.bytes, cx.redirect, or cx.render for a template. ? turns any error into the right error page.

With no host declared, the site answers localhost (and 127.0.0.1, [::1], *.localhost) only, so it works on a laptop as it is and a name someone else points at the server is never the site. Declare the real host before deploying (.host("example.com")), or .any_host() behind a front that checked it.

A site with a host, a folder, and a form:

use web_net::{App, Route, get};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let app = App::builder()
        .host("example.com")
        .alias("www.example.com")
        .base("/blog")
        .route(get("/", |cx| cx.html(200, "<h1>Blog</h1>")))
        .route(get("/posts/:slug", |cx| cx.text(200, &cx.param("slug")?)))
        .route(Route::at("/contact")
            .get(|cx| cx.html(200, "<form method=post></form>"))
            .post(|cx| cx.redirect(303, "/"))
            // Unsafe methods need Builder::csrf(..) or an exemption with a reason.
            .csrf_exempt("demo: the POST changes nothing"))
        .build()?;

    app.listen("127.0.0.1:8080")?.wait();
    Ok(())
}

App::listen uses serve-net's defaults. For TLS, several listeners, or other limits, an App is a serve-net HttpService: serve_net::Server::builder()...serve(app).

The request path

Step What happens Answers
site the host picks the site; the base path is removed 421 unknown host; 301 alias or HTTPS
url decoded once, one canonical spelling, dot and empty segments removed 400 refused; 301/308 to the canonical path
authenticate the gate says who is asking the gate's denial
route a trie compiled at build 404
method HEAD from GET, OPTIONS automatic 405 with Allow, 501
authorize the route's rule, unless it is public 303 to sign-in, 401, 403, 404
hooks, handler only the hooks registered for the route the handler's error
render the engine reads a typed view; no JSON 500
errors one function: the engine's error template or a built-in page

Plugging in

  • Authentication and authorization: implement Gate (authenticate, authorize) and Identity, then App::with_gate(gate). Every route then declares .public() or .allow(rule), or the app does not build; a group's rule holds for every route in it. A front layer that authenticates on its own passes the identity to App::respond and forwards serve-net's done to the app.
  • Templates: implement Render. A typed engine compiles against your own types (type View<'a> = Page<'a>); a dynamic one reads any View. App::builder().render(engine) (or App::with_gate(gate).render(engine)). Engines live in their own crates; web-net depends on none.
  • Detection and logs: implement Events (every refusal, denial, failure, panic) and Checkpoint (in-flight revocation).

Built in: static files (Assets: validators, ranges, precompressed variants, no escape from the folder), health and readiness paths, strict security headers by default (SecurityHeaders), cookies out through cookie-net, and a 304 fast path for pages (Context::not_modified).

Forms and uploads through form-net (Context::form, Context::upload), CSRF through csrf-sec on every unsafe method (origin policy and a session-bound token; a route without either does not build unless it is exempt with a reason), allowlisted external redirects, and If-Match preconditions.

Negotiation (JSON error replies, Context::accept, Context::accept_language), CORS per route or group, gzip compression, cache policies with a report a cache in front can trust (App::respond), and several sites in one table (Sites).

Performance: the warm request path allocates nothing, route lookup does not grow with the number of routes, and web-net adds well under a microsecond of its own work per request (docs/PERFORMANCE.md).

Security: every threat the plan names has an attack test that must fail, the path rules have property tests, whole requests are fuzzed, and docs/SECURITY.md lists each defence.

examples/site.rs puts it together: sessions, sign-in with CSRF, an upload, static files, and pages rendered by Luminess.

Coming next (see the roadmap): 0.10.x releases until a site has run on web-net in production; 1.0 after that.

Contributing

See dev/DIRECTIVES.md for engineering standards and the definition of done. Before a PR: cargo fmt --all, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features must be clean.