Skip to content

Latest commit

 

History

History
120 lines (86 loc) · 7.97 KB

File metadata and controls

120 lines (86 loc) · 7.97 KB

Architecture — webtyp/server/httpd

The httpd subpackage provides a high-level, "batteries-included" HTTP server implementation that implements the webtyp.com/router contract.

Relationship with internalStrategy

The development mode's internalStrategy (in-memory server) consumes the httpd.NewRouter adapter to provide consistent routing behavior between development and production.

The server entry point is generated, not written

A project does not contain an HTTP main(). maingen.go writes one to .build/server/main.go — inside the project, because outside the module tree the Go toolchain cannot resolve the project's own imports — and the External strategy compiles and runs it.

The generated main imports the project's routes package and calls routes.Register on the server it builds, so development and production serve the same route set from the same source. The previous templates/server_basic.md scaffold is gone: it produced a committed web/server.go whose routes had to be kept in sync by hand with the ones the dev process registered, and they drifted.

Which strategy runs is decided by one file: a project with routes/routes.go gets the compiled server; a project without one gets the in-memory asset server and compiles nothing. A web/server.go that a developer writes themselves still wins — the tool never overrides code the user owns — but nothing generates it.

Components

  • adapter.go: Implementation of router.Router, router.Context, etc., mapping them to net/http. httpd produces router.ContextKeyRemoteAddr from Request.RemoteAddr.
  • middleware.go: Built-in Gzip and NoCache middlewares.
  • static.go: Static file serving from PublicDir. Each file carries Cache-Control from pwa.CacheControl (content-hashed names immutable, /artifacts/ never cached, everything else revalidated) unless NoCache is on.
  • enforce.go: RBAC enforcement based on Requires metadata.
  • tls.go / localcert.go / ca.go: AutoCert (Let's Encrypt), custom Cert/Key, PlainHTTP, and the local CA default (LocalCA).
  • spki.go: LocalCertSPKI(), the pin the development browser trusts.
  • routes_endpoint.go: Optional JSON endpoint at /_routes listing all registered routes.
  • httpd.go: Core Server orchestrator.

Pattern Handling and Introspection Delegations

  1. Pattern Matching & Extraction: Delegated to net/http.ServeMux. Routes registered as "GET /api/items/{id}" natively match parameters in Go 1.22+, accessible via ctx.Param(name).
  2. Pattern Validation: Delegated to webtyp/router via router.ValidatePattern. This server rejects patterns with wildcards (e.g., {name...}) at registration time so that patterns unsupported by edge runtimes fail early on the dev server.
  3. Introspection Endpoint: Delegated to router.MountIntrospection at router.IntrospectionPath (/_routes). This repo supplies the Config.RoutesEndpoint boolean toggle and the Config.Policy describer.

Design Goals

  1. Simple Entry Point: httpd.New(config).Mount(modules).ListenAndServe() is the only way to start the server.
  2. Standard Library Based: Uses net/http internally but doesn't expose it in the public API.
  3. Fails Fast: Validates configuration (like TLS modes or RBAC requirements) at startup rather than at runtime.

RBAC and Public Assets

The server follows a secure-by-default model where routes are private unless explicitly marked as Public(). However, for a smooth development experience:

  • Static Assets: Files served from PublicDir (e.g., WASM binaries, JS, CSS) are always public. They bypass the RBAC middleware.
  • Root Path (/) in Development: The internal strategy registers a default public route for / that serves PublicDir/index.html or a diagnostic message. This ensures that the frontend application is accessible immediately without manual RBAC configuration.

TLS: HTTPS by default

The zero value of TLSConfig serves HTTPS with the local certificate authority in development and production. Setting PlainHTTP: true is the only way to serve plain HTTP (e.g. when a proxy in front terminates TLS). A page served by httpd is always a secure context, on localhost and on a LAN.

The CA route (/__webtyp/ca) is mounted automatically by httpd whenever it serves the local CA.

It generates a two-level chain, the same shape mkcert and Caddy's internal PKI use:

  • The CA (httpd/localcert.go, ensureLocalCA) — IsCA: true, KeyUsageCertSign | KeyUsageCRLSign, MaxPathLen: 0, no subject alternative names, long-lived. This is the file a device installs to trust the server.
  • The leaf (httpd/localcert.go, ensureLocalCert) — signed by the CA, IsCA: false, serverAuth, and a SAN set covering localhost, the lowercased host computer name, 127.0.0.1, ::1 and every non-loopback IPv4 of the host's interfaces. It is regenerated when that address set changes or when machine name changes, and renews itself automatically 30 days before expiry without requiring a server restart.

A single self-signed certificate cannot do this job: a leaf has CA:FALSE, and iOS and Android refuse to use it as a trust anchor — the device shows no error, the page simply keeps failing after the user has followed every instruction.

Two ways a client trusts it, and they are not interchangeable

Client Mechanism Trusts
The development browser --ignore-certificate-errors-spki-list with LocalCertSPKI() exactly the leaf's public key
A phone on the LAN installs the CA from CAPath (/__webtyp/ca) anything the CA signs

LocalCertSPKI() returns the leaf's SPKI hash, never the CA's. Chrome matches the flag against any certificate in the presented chain, so pinning the CA would appear to work — and would silently grant the browser trust in every certificate that CA ever signs, surviving a leaf rotation unnoticed.

Installing the CA into the developer's own OS trust store

On first generation the CA is also installed into the host's trust stores via github.com/smallstep/truststore, so that clients which were not launched by webtyp.com/devbrowser — a second browser, curl, a native client — do not see a warning. The step is best-effort: on Linux it shells out through sudo and may prompt, and any failure is logged as a warning rather than aborting the listen. Setting WEBTYP_LOCALCERT_SKIP_TRUSTSTORE to any non-empty value skips it entirely; test runners and CI set it. Nothing about serving TLS depends on it — the SPKI flag and CAPath still work when it is skipped or fails.

Why that dependency rather than mkcert or hand-written per-platform code is argued in DESIGN.md.

On iOS, installing a profile is two steps and the platform announces only the first: install it (Settings → Profile Downloaded), then enable it under Settings → General → About → Certificate Trust Settings. A profile installed but not trusted behaves exactly like no profile at all.

Windows: open the downloaded file and install it in "Trusted Root Certification Authorities".

HTTP/2 and Streaming (SSE, WebSockets)

Long-lived connections (such as SSE streams or WebSockets registered via r.Stream(...)) have specialized transport requirements:

  1. WriteTimeout Bypass: Go's http.Server.WriteTimeout is 15s by default. In httpd, stream handlers invoke http.NewResponseController(w).SetWriteDeadline(time.Time{}) to disable this timeout for the stream's lifetime, preventing abrupt connection terminations.
  2. Hop-by-Hop Header Sanitization: In HTTP/2 (RFC 9113 §8.2.2), connection-specific headers (Connection, Keep-Alive, Proxy-Connection, Transfer-Encoding, Upgrade) are strictly prohibited and cause PROTOCOL_ERROR. httpd automatically filters these headers when r.ProtoMajor >= 2.
  3. Gzip Bypass: Any response with Content-Type: text/event-stream bypasses gzip compression to prevent buffering delays and HTTP/2 framing issues.