secure dev tunnel: strip /__nextjs debug endpoints while keeping HMR
This commit is contained in:
parent
12ff3e70e9
commit
f5a913201a
5 changed files with 181 additions and 17 deletions
115
scripts/dev-server.mjs
Normal file
115
scripts/dev-server.mjs
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
// Custom Next.js dev server that keeps live updates but drops the dev-only
|
||||
// debug surface for anyone who is not on localhost.
|
||||
//
|
||||
// WHY THIS EXISTS
|
||||
// `beta.dvorakmortonxd.dev` is an unauthenticated tunnel straight to the dev
|
||||
// server, and a Next.js dev server is not meant to be public. It exposes a set
|
||||
// of unauthenticated debug endpoints under `/__nextjs_*` (open-the-editor,
|
||||
// stack-frame/source-map lookups, node debugger attach, server restart, …) that
|
||||
// you really do not want on the internet. Next.js wires those endpoints in
|
||||
// unconditionally, so `next.config.ts` cannot turn them off.
|
||||
//
|
||||
// This server sits in front of Next's request handler and returns 404 for
|
||||
// `/__nextjs*` and source maps when the request did not arrive over loopback.
|
||||
// Everything the browser needs for live updates still goes through:
|
||||
// - normal app routes
|
||||
// - `/_next/hmr` (the HMR / Fast Refresh WebSocket)
|
||||
//
|
||||
// So `localhost:3000` keeps the full dev experience, while the LAN/tunnel get
|
||||
// a plain, live-reloading version of the site.
|
||||
//
|
||||
// Run with `npm run dev`. To bypass it entirely, use `npx next dev --turbopack`.
|
||||
|
||||
import { createServer } from "node:http";
|
||||
import next from "next";
|
||||
|
||||
const dev = process.env.NODE_ENV !== "production";
|
||||
const port = Number.parseInt(process.env.PORT || "3000", 10);
|
||||
const hostname = process.env.HOSTNAME || "0.0.0.0";
|
||||
|
||||
// Next's dev-only internals all share this prefix:
|
||||
// /__nextjs_launch-editor, /__nextjs_original-stack-frames,
|
||||
// /__nextjs_source-map, /__nextjs_server_status,
|
||||
// /__nextjs_attach-nodejs-inspector, /__nextjs_restart_dev, …
|
||||
const DEV_DEBUG_PREFIX = "/__nextjs";
|
||||
|
||||
function isLoopback(socket) {
|
||||
const address = socket.localAddress || "";
|
||||
return (
|
||||
address === "127.0.0.1" ||
|
||||
address === "::1" ||
|
||||
address === "::ffff:127.0.0.1" ||
|
||||
address.startsWith("127.")
|
||||
);
|
||||
}
|
||||
|
||||
// A request that arrived through a proxy/tunnel (Cloudflare or otherwise) is
|
||||
// never a local dev request, even if the proxy itself runs on this machine and
|
||||
// therefore connects over loopback.
|
||||
function hasForwardedHeaders(req) {
|
||||
const headers = req.headers;
|
||||
return Boolean(
|
||||
headers["cf-connecting-ip"] ||
|
||||
headers["cf-ray"] ||
|
||||
headers["x-forwarded-for"] ||
|
||||
headers["x-forwarded-proto"] ||
|
||||
headers["forwarded"],
|
||||
);
|
||||
}
|
||||
|
||||
function isLocalDebugRequest(req) {
|
||||
return isLoopback(req.socket) && !hasForwardedHeaders(req);
|
||||
}
|
||||
|
||||
function isDevDebugRequest(req) {
|
||||
let pathname = req.url || "/";
|
||||
try {
|
||||
pathname = new URL(req.url || "/", "http://localhost").pathname;
|
||||
} catch {
|
||||
// Fall back to the raw URL if it is not parseable.
|
||||
}
|
||||
return (
|
||||
pathname.startsWith(DEV_DEBUG_PREFIX) || pathname.endsWith(".map")
|
||||
);
|
||||
}
|
||||
|
||||
let handle;
|
||||
const server = createServer((req, res) => {
|
||||
if (!isLocalDebugRequest(req) && isDevDebugRequest(req)) {
|
||||
res.statusCode = 404;
|
||||
res.setHeader("content-type", "text/plain; charset=utf-8");
|
||||
res.setHeader("cache-control", "no-store");
|
||||
res.end("Not found");
|
||||
return;
|
||||
}
|
||||
// `handle` is assigned right after `getRequestHandler()` below; requests
|
||||
// cannot arrive before `listen`, which happens after that assignment.
|
||||
handle(req, res);
|
||||
});
|
||||
|
||||
// Passing our HTTP server in lets Next attach its HMR upgrade handler to it.
|
||||
const app = next({ dev, turbopack: true, hostname, port, httpServer: server });
|
||||
handle = app.getRequestHandler();
|
||||
|
||||
await app.prepare();
|
||||
|
||||
server.listen(port, hostname, () => {
|
||||
console.log(`> dev server ready on http://localhost:${port}`);
|
||||
console.log(
|
||||
"> dev debug endpoints are localhost-only (404 on the LAN/tunnel); HMR still works",
|
||||
);
|
||||
});
|
||||
|
||||
function shutdown() {
|
||||
// Drop idle keep-alive sockets so Ctrl+C exits promptly instead of waiting
|
||||
// on lingering connections.
|
||||
server.closeAllConnections?.();
|
||||
server.close(() => {
|
||||
app.close().finally(() => process.exit(0));
|
||||
});
|
||||
// Hard exit if Next's cleanup hangs.
|
||||
setTimeout(() => process.exit(0), 3000).unref();
|
||||
}
|
||||
|
||||
process.on("SIGINT", shutdown);
|
||||
process.on("SIGTERM", shutdown);
|
||||
Loading…
Add table
Add a link
Reference in a new issue