diff --git a/PLAN.md b/PLAN.md new file mode 100644 index 0000000..37fe6d2 --- /dev/null +++ b/PLAN.md @@ -0,0 +1,54 @@ +# Plan: Fix local demo server connection refused on port 8080 + +## Problem Analysis + +The `server.js` file has two issues preventing the demo from working: + +### Issue 1: IPv6/IPv4 Binding +`server.listen(port)` (line 10) is called without a host argument. Per [Node.js docs](https://nodejs.org/api/net.html#serverlistenport-host-backlog-callback), when host is omitted the server binds to `::` (IPv6 unspecified address) when IPv6 is available. While most OSes support dual-stack (accepting both IPv4 and IPv6 on `::`), some environments (including containers and sandboxes) may have IPv6 disabled or `localhost` resolving only to `127.0.0.1`, causing "connection refused". + +**Fix**: Pass `'0.0.0.0'` as the host to `server.listen()` to explicitly bind to all IPv4 interfaces. + +### Issue 2: No Static File Serving +The current server handler only returns a plain text response (`"Hello from mogterm2!"`). It doesn't serve the HTML, CSS, or JS files that make up the demo (`demo/index.html`, `src/*.js`, `src/*.css`, `index.html`). Even if the binding issue were fixed, navigating to `http://localhost:8080` would show plain text, not the demo page. + +**Fix**: Replace the stub handler with a static file server that: +- Serves files from the project root directory +- Maps `/` to `index.html` (the project already has one at root) +- Returns correct `Content-Type` headers based on file extension +- Returns 404 for missing files +- Uses only Node.js built-in modules (`node:fs`, `node:path`, `node:http`) — no new dependencies + +## Scope + +This is a **single-agent** task. Both fixes are in the same file (`server.js`) and are tightly coupled — you can't meaningfully test one without the other. + +## Files to Change + +- `server.js` — the only file that needs modification + +## Implementation Steps + +1. Add imports for `node:fs` and `node:path` +2. Define a MIME type map for file extensions used in the project (`.html`, `.css`, `.js`, `.json`, `.ts`) +3. Replace the request handler to: + - Resolve the requested URL path to a filesystem path relative to the project root + - Default `/` to `/index.html` + - Prevent path traversal (reject paths containing `..`) + - Read the file and serve it with the correct Content-Type + - Return 404 for missing files +4. Change `server.listen(port, ...)` to `server.listen(port, '0.0.0.0', ...)` +5. Update the console.log to show the full URL including `http://0.0.0.0:${port}` + +## Validation + +- Start the server with `node server.js` +- Verify `curl http://127.0.0.1:8080/` returns HTML content +- Verify `curl http://127.0.0.1:8080/demo/index.html` returns the demo page +- Verify `curl http://127.0.0.1:8080/src/terminal.js` returns JavaScript +- Run existing tests: `npx tsx --test test/**/*.test.ts` to ensure no regressions + +## Sources + +- [Node.js net.Server.listen() docs](https://nodejs.org/api/net.html#serverlistenport-host-backlog-callback) — confirms `::` default binding behavior +- [Node.js http.createServer() docs](https://nodejs.org/api/http.html#httpcreateserveroptions-requestlistener) — static file serving pattern diff --git a/server.js b/server.js index 607ee84..276aea5 100644 --- a/server.js +++ b/server.js @@ -1,12 +1,43 @@ import { createServer } from "node:http"; +import { readFile } from "node:fs"; +import { dirname, join, extname } from "node:path"; +import { fileURLToPath } from "node:url"; +const __dirname = dirname(fileURLToPath(import.meta.url)); const port = process.env.PORT || 8080; +const mimeTypes = { + ".html": "text/html", + ".css": "text/css", + ".js": "application/javascript", + ".json": "application/json", + ".ts": "text/plain", +}; + const server = createServer((req, res) => { - res.writeHead(200, { "Content-Type": "text/plain" }); - res.end("Hello from mogterm2!\n"); + const pathname = new URL(req.url, "http://localhost").pathname; + + if (pathname.includes("..")) { + res.writeHead(403); + res.end("Forbidden"); + return; + } + + const filePath = join(__dirname, pathname === "/" ? "/index.html" : pathname); + const ext = extname(filePath); + const contentType = mimeTypes[ext] || "application/octet-stream"; + + readFile(filePath, (err, data) => { + if (err) { + res.writeHead(404); + res.end("Not Found"); + return; + } + res.writeHead(200, { "Content-Type": contentType }); + res.end(data); + }); }); -server.listen(port, () => { - console.log(`Server listening on port ${port}`); +server.listen(port, "0.0.0.0", () => { + console.log(`Server listening on http://0.0.0.0:${port}`); }); diff --git a/tasks.json b/tasks.json new file mode 100644 index 0000000..89cda93 --- /dev/null +++ b/tasks.json @@ -0,0 +1,6 @@ +{ + "mode": "single", + "claudeMd": "# Project Context\n\nYou are fixing `server.js` in a Node.js terminal emulator project (mogterm2). The project uses `\"type\": \"module\"` in package.json, so all imports must use ESM syntax (`import ... from ...`).\n\n## Bug\n\nThe demo server has two problems:\n1. `server.listen(port)` binds to `::` (IPv6 only) — must bind to `0.0.0.0` for IPv4 access\n2. The request handler only returns plain text — must serve static files from the project root\n\n## What to Change\n\nOnly modify `server.js`. The current file is:\n```js\nimport { createServer } from \"node:http\";\n\nconst port = process.env.PORT || 8080;\n\nconst server = createServer((req, res) => {\n res.writeHead(200, { \"Content-Type\": \"text/plain\" });\n res.end(\"Hello from mogterm2!\\n\");\n});\n\nserver.listen(port, () => {\n console.log(`Server listening on port ${port}`);\n});\n```\n\n## Requirements\n\n1. Add ESM imports for `node:fs`, `node:path`, and `node:url` (for `fileURLToPath` to get `__dirname` equivalent)\n2. Compute the project root using `path.dirname(fileURLToPath(import.meta.url))`\n3. Define a MIME type map: `.html` → `text/html`, `.css` → `text/css`, `.js` → `application/javascript`, `.json` → `application/json`, `.ts` → `text/plain`, default → `application/octet-stream`\n4. Replace the request handler to serve static files:\n - Parse `req.url` to get the pathname\n - Map `/` to `/index.html`\n - Reject paths containing `..` with a 403\n - Resolve the path relative to project root\n - Read the file with `fs.readFile`\n - Serve with correct Content-Type; 404 if file not found\n5. Change `server.listen(port, ...)` to `server.listen(port, '0.0.0.0', ...)`\n6. Update the console.log to show `http://0.0.0.0:${port}`\n\n## Conventions\n- ESM imports only (the project has `\"type\": \"module\"`)\n- Use `node:` protocol prefix for built-in modules (matches existing code)\n- Keep it minimal — no dependencies, no over-engineering\n- Conventional Commits for the commit message: `fix(server): bind to 0.0.0.0 and serve static files`\n\n## Validation\n- Start server: `node server.js &`\n- Test: `curl -s http://127.0.0.1:8080/ | head -5` should return HTML\n- Test: `curl -s http://127.0.0.1:8080/demo/index.html | head -5` should return HTML\n- Test: `curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8080/nonexistent` should return 404\n- Kill the server after testing\n- Run existing tests: `npx tsx --test test/**/*.test.ts`\n\n## Gotchas\n- Must use `fileURLToPath(import.meta.url)` instead of `__dirname` (ESM has no `__dirname`)\n- `fs.readFile` callback-style is fine; no need for promises\n- URL pathname may include query strings — use `new URL(req.url, 'http://localhost').pathname` to extract just the path", + "subtasks": [], + "integration": null +}