diff --git a/README.md b/README.md index efb83b3..e1332df 100644 --- a/README.md +++ b/README.md @@ -238,7 +238,7 @@ to Wild PC's computed values. Secrets live in `~/.wildpc/secrets/` (never in a r Every gateway-exposed deployment gets its own subdomain — `.` — routed to it by the Caddy gateway (there are no path-prefix routes). Exposure is a single checkbox: `proxy: true` on a service, while a static deployment is inherently -served. The dashboard is `wildpc.` and the API `wildpc-api.`; on a +served. The dashboard is `wild-pc.` and the API `wildpc-api.`; on a node with no domain, `:9000` serves the dashboard plus a `/api` proxy. `gateway.tls` is a per-node choice: `off` (plain HTTP on `:9000`) or `acme` (a real @@ -266,7 +266,7 @@ programs (CLI, API, gateway) know Wild PC internals. ## Dashboard & API -The **dashboard** (`app/`, served at `wildpc.` or `http://localhost:9000`) +The **dashboard** (`app/`, served at `wild-pc.` or `http://localhost:9000`) lists programs and deployments, edits their config, drives lifecycle, shows the gateway route table and logs, and hosts the agent terminal dock. diff --git a/app/src/services/api/client.ts b/app/src/services/api/client.ts index 1ef9dba..d7e7b8a 100644 --- a/app/src/services/api/client.ts +++ b/app/src/services/api/client.ts @@ -1,5 +1,5 @@ // Resolve the wildpc-api base URL. The gateway serves each service at its own -// subdomain (.), so when the dashboard runs at wildpc. +// subdomain (.), so when the dashboard runs at wild-pc. // the API lives at wildpc-api. — a cross-origin call (wildpc-api allows // CORS *). When served at a bare host (dev, or the off-mode :9000 gateway), the // API is reachable same-origin at /api. diff --git a/bootstrap/deployments/statics/wild-pc.yaml b/bootstrap/deployments/statics/wild-pc.yaml new file mode 100644 index 0000000..be4fb44 --- /dev/null +++ b/bootstrap/deployments/statics/wild-pc.yaml @@ -0,0 +1,6 @@ +program: wild-pc +description: Wild PC web app +requires: +- kind: deployment + ref: wildpc-api +manager: caddy diff --git a/bootstrap/deployments/statics/wildpc.yaml b/bootstrap/deployments/statics/wildpc.yaml deleted file mode 100644 index 5dbe026..0000000 --- a/bootstrap/deployments/statics/wildpc.yaml +++ /dev/null @@ -1,3 +0,0 @@ -program: wildpc -description: Wild PC web app -manager: caddy diff --git a/bootstrap/programs/wildpc.yaml b/bootstrap/programs/wild-pc.yaml similarity index 100% rename from bootstrap/programs/wildpc.yaml rename to bootstrap/programs/wild-pc.yaml diff --git a/cli/src/wildpc_cli/commands/doctor.py b/cli/src/wildpc_cli/commands/doctor.py index 8cf3ca4..4a9f580 100644 --- a/cli/src/wildpc_cli/commands/doctor.py +++ b/cli/src/wildpc_cli/commands/doctor.py @@ -28,7 +28,7 @@ _ICON = { _GATEWAY = "wildpc-gateway" _API = "wildpc-api" -_DASHBOARD = "wildpc" +_DASHBOARD = "wild-pc" @dataclass diff --git a/core/src/wildpc_core/generators/caddyfile.py b/core/src/wildpc_core/generators/caddyfile.py index e8a08fd..74f1c37 100644 --- a/core/src/wildpc_core/generators/caddyfile.py +++ b/core/src/wildpc_core/generators/caddyfile.py @@ -254,6 +254,23 @@ def _host_static_block(label: str, host: str, serve_dir: str, spa: bool) -> list ] +def _no_route_fallback() -> list[str]: + """The trailing catch-all for a wildcard site: an explicit 404 for a host that + matched `*.` but no route. + + Without it Caddy runs off the end of the site block and returns a bare + `200 Content-Length: 0` — a typo'd or stale subdomain looks *up* to a browser + (TLS negotiates, the request succeeds) while rendering nothing. A `handle` + with no matcher is the fallback of its handle group, so it only fires when + every `@host_*` matcher above missed.""" + return [ + " handle {", + ' respond "no wildpc route for this host" 404', + " }", + "", + ] + + def _public_site_block(host: str, kind: str, target: str, spa: bool = True) -> list[str]: """A standalone Caddy site for a custom ``public_host`` (apex or another zone). @@ -281,7 +298,7 @@ def _public_site_block(host: str, kind: str, target: str, spa: bool = True) -> l # Wild PC's own control plane: the dashboard frontend and the API it calls. These # names are the subdomains they're published at in acme mode, and the pair served # on the : site in off mode (no domain → no subdomains). -_DASHBOARD = "wildpc" +_DASHBOARD = "wild-pc" _API = "wildpc-api" @@ -346,6 +363,7 @@ def generate_caddyfile_from_registry( lines += _host_remote_block(r.name or r.address, host, r.target) else: lines += _host_matcher_block(r.name or r.address, host, r.target) + lines += _no_route_fallback() lines.append("}") lines.append("") # Public exposure: public deployments are also reachable by their public @@ -371,6 +389,7 @@ def generate_caddyfile_from_registry( lines += _host_remote_block(label, host, r.target) else: lines += _host_matcher_block(label, host, r.target) + lines += _no_route_fallback() lines.append("}") lines.append("") for r in custom_pub: diff --git a/core/tests/test_caddyfile.py b/core/tests/test_caddyfile.py index 46cc70c..adecbe2 100644 --- a/core/tests/test_caddyfile.py +++ b/core/tests/test_caddyfile.py @@ -101,7 +101,16 @@ class TestAcmeMode: def test_port_9000_redirects_to_dashboard(self) -> None: cf = generate_caddyfile_from_registry(_acme({"api": _dep(9020, expose=True, name="api")})) assert ":9000 {" in cf - assert "redir https://wildpc.example.com{uri}" in cf + assert "redir https://wild-pc.example.com{uri}" in cf + + def test_unmatched_host_404s_instead_of_empty_200(self) -> None: + """A host that matches `*.` but no route must 404. Without the + trailing catch-all Caddy falls off the end of the site block and returns + an empty 200, so a typo'd subdomain looks up but renders nothing.""" + cf = generate_caddyfile_from_registry(_acme({"api": _dep(9020, expose=True, name="api")})) + wildcard = cf.split("*.example.com {", 1)[1].split("\n}", 1)[0] + assert "handle {" in wildcard + assert 'respond "no wildpc route for this host" 404' in wildcard def test_no_path_routes(self) -> None: cf = generate_caddyfile_from_registry(_acme({"api": _dep(9020, expose=True, name="api")})) diff --git a/docs/registry.md b/docs/registry.md index 4d6ebe0..864adf4 100644 --- a/docs/registry.md +++ b/docs/registry.md @@ -404,14 +404,21 @@ route — differing only in whether the target is files on disk or a live proces The table is shown by `wildpc gateway status`, the dashboard Gateway panel, and `GET /gateway`; the Caddyfile is generated from it. -**The dashboard and its API.** `wildpc` (the dashboard frontend) and `wildpc-api` -are just two such subdomains (`wildpc.`, `wildpc-api.`); the +**The dashboard and its API.** `wild-pc` (the dashboard frontend) and `wildpc-api` +are just two such subdomains (`wild-pc.`, `wildpc-api.`); the dashboard calls the API **cross-origin** (wildpc-api allows CORS `*`). The bare gateway port (`:9000`) redirects to the dashboard subdomain. On a node with **no domain** (`gateway.tls: off`), there are no subdomains, so `:9000` serves just the control plane — the dashboard at `/` plus a `/api` reverse-proxy to wildpc-api — and other services stay port-only. +**Unrouted subdomains 404.** In `acme` mode every route lives inside one +`*.` wildcard site, so a host that resolves but has no route still matches +the site and completes its TLS handshake. Each wildcard site therefore ends with an +unmatched `handle` that responds `404` — without it Caddy runs off the end of the +block and returns a bare `200` with an empty body, which makes a typo'd or renamed +subdomain look **up** in a browser while rendering nothing. + #### Host routes need DNS, and the gateway is HTTP-only A host route only does something once `` resolves **to this node**. For a