diff --git a/README.md b/README.md index 5e15103..b11e9ee 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,8 @@ Needs Node 24 or later, and nothing else: Jeview has no dependencies to install. ``` That finds a suitable Node (on your PATH, or installed by nvm or Homebrew), starts Jeview and opens the viewer at -http://127.0.0.1:4777/. `npm start` does the same without opening a browser. Add your TypeSafe API key (the key icon, +http://127.0.0.1:4777/ (or http://jeview.localhost:4777/, a name your browser already knows). `npm start` does the same +without opening a browser. Add your TypeSafe API key (the key icon, top right). The first time you open it, the viewer explains itself. ## See it working diff --git a/src/jeview.ts b/src/jeview.ts index 6933e17..217813d 100644 --- a/src/jeview.ts +++ b/src/jeview.ts @@ -24,7 +24,9 @@ const PAGE = 5000, IDS_LISTED = 1000; // summaries in one answer: a page of the const REQUEST_DROP = new Set(["host", "connection", "keep-alive", "proxy-connection", "transfer-encoding", "upgrade", "te", "trailer", "content-length", "accept-encoding", "authorization", "cookie", "origin", "referer"]); // fetch has already decoded the body, so its length and encoding no longer describe what is sent on const RESPONSE_DROP = new Set(["connection", "keep-alive", "transfer-encoding", "content-length", "content-encoding"]); -const LOOPBACK = /^(127\.0\.0\.1|localhost|\[::1\])(:\d+)?$/; +// the names this machine goes by. Any name ending in .localhost is one of them: that ending is reserved for the machine +// itself, so no other site can have it, and http://jeview.localhost:4777/ opens the viewer with no hosts entry +const LOOPBACK = /^(127\.0\.0\.1|\[::1\]|([a-z0-9-]+\.)*localhost)(:\d+)?$/; /** A page on another site can POST here without asking first, and the call would go out with the user's key. A browser * names the page's site in Origin ("null" for a sandboxed one); a caller that is not a page sends none. */ const foreign = (origin: string | undefined) => origin !== undefined && !LOOPBACK.test(origin.replace(/^https?:\/\//, "")); diff --git a/test/jeview.test.ts b/test/jeview.test.ts index 8b97a91..ae8d170 100644 --- a/test/jeview.test.ts +++ b/test/jeview.test.ts @@ -175,6 +175,9 @@ test("the viewer answers loopback hosts only, serves its page, and a non-JSON bo const status = (host: string) => new Promise((accept, reject) => httpRequest({ host: "127.0.0.1", port, path: "/_/api/records", headers: { host } }, (res) => { res.resume(); accept(res.statusCode ?? 0); }).on("error", reject).end()); assert.equal(await status(`localhost:${port}`), 200); assert.equal(await status("attacker.example"), 403); + // a name ending in .localhost is this machine and nobody else's; a name that only looks like one is not + assert.deepEqual(await Promise.all([`jeview.localhost:${port}`, "my.jeview.localhost"].map(status)), [200, 200]); + assert.deepEqual(await Promise.all(["localhost.attacker.example", `jeview.localhost.attacker.example:${port}`, "attackerlocalhost", "jeview.localhost@attacker.example", ".localhost"].map(status)), [403, 403, 403, 403, 403]); const page = await fetch(`${base}/`); assert.equal(page.headers.get("content-type"), "text/html; charset=utf-8"); assert.match(page.headers.get("content-security-policy") ?? "", /script-src 'self'/);