Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion src/jeview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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?:\/\//, ""));
Expand Down
3 changes: 3 additions & 0 deletions test/jeview.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<number>((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'/);
Expand Down
Loading