| 182 | } |
| 183 | |
| 184 | async function handleReload(req: Request): Promise<Response> { |
| 185 | let body: any; |
| 186 | try { |
| 187 | body = await req.json(); |
| 188 | } catch { |
| 189 | return Response.json({ error: "Invalid JSON" }, { status: 400 }); |
| 190 | } |
| 191 | |
| 192 | const newHtmlPath = body.html; |
| 193 | if (!newHtmlPath || !fs.existsSync(newHtmlPath)) { |
| 194 | return Response.json( |
| 195 | { error: `HTML file not found: ${newHtmlPath}` }, |
| 196 | { status: 400 }, |
| 197 | ); |
| 198 | } |
| 199 | |
| 200 | // Security: resolve symlinks and validate the reload path is a FILE |
| 201 | // inside the allowed directory (anchored to the initial HTML file's |
| 202 | // parent). Prevents path traversal via /api/reload reading arbitrary |
| 203 | // files. A path resolving to the allowedDir itself (a directory) used |
| 204 | // to pass the guard and then crash readFileSync with EISDIR — reject |
| 205 | // it explicitly with a clear 400 instead. |
| 206 | const resolvedReload = fs.realpathSync(path.resolve(newHtmlPath)); |
| 207 | if (!resolvedReload.startsWith(allowedDir + path.sep)) { |
| 208 | return Response.json( |
| 209 | { error: `Path must be within: ${allowedDir}` }, |
| 210 | { status: 403 }, |
| 211 | ); |
| 212 | } |
| 213 | if (!fs.statSync(resolvedReload).isFile()) { |
| 214 | return Response.json( |
| 215 | { error: `Path must be a file, not a directory: ${newHtmlPath}` }, |
| 216 | { status: 400 }, |
| 217 | ); |
| 218 | } |
| 219 | |
| 220 | // Swap the HTML content |
| 221 | htmlContent = fs.readFileSync(resolvedReload, "utf-8"); |
| 222 | state = "serving"; |
| 223 | |
| 224 | console.error(`SERVE_RELOADED: html=${newHtmlPath}`); |
| 225 | |
| 226 | // Reset timeout |
| 227 | if (timeoutTimer) clearTimeout(timeoutTimer); |
| 228 | timeoutTimer = setTimeout(() => { |
| 229 | console.error(`SERVE_TIMEOUT: after=${timeout}s`); |
| 230 | server.stop(); |
| 231 | process.exit(1); |
| 232 | }, timeout * 1000); |
| 233 | |
| 234 | return Response.json({ reloaded: true }); |
| 235 | } |
| 236 | |
| 237 | // Keep the process alive |
| 238 | await new Promise(() => {}); |