summaryrefslogtreecommitdiffhomepage
path: root/scripts/vite-network-guard.ts
blob: 7fe9eca35c01e99c766f2db0ec331ec315f7f686 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
import type { IncomingMessage } from "node:http";
import type { Plugin, UserConfig } from "vite";
import { checkDevRequest, type DevNetworkPolicy, normalizeHostHeader } from "./network-policy";

/** Explicitly protect BOTH serving modes; preview must not inherit permissive CORS alone. */
export function applyViteNetworkPolicy(config: UserConfig, network: DevNetworkPolicy): UserConfig {
  return {
    ...config,
    plugins: [devNetworkGuard(network), ...(config.plugins ?? [])],
    server: {
      ...config.server,
      allowedHosts: [...network.allowedHosts],
      // Safe reflection only after our pre-middleware validates the Origin.
      cors: { origin: true },
    },
    preview: {
      ...config.preview,
      allowedHosts: [...network.allowedHosts],
      cors: { origin: true },
    },
  };
}

export function devNetworkGuard(policy: DevNetworkPolicy): Plugin {
  const check = (req: IncomingMessage) =>
    checkDevRequest(
      {
        localAddress: req.socket.localAddress,
        host: req.headers.host,
        origin: req.headers.origin,
      },
      policy,
    );
  // Installed before Vite's CORS, host check and static-file middleware in
  // either mode. No post-hook: even preflights and error responses are guarded.
  const middleware: import("vite").Connect.NextHandleFunction = (req, res, next) => {
    const decision = check(req);
    if (decision !== "ok") {
      res.writeHead(decision.status, { "Content-Type": "text/plain" });
      res.end(decision.reason);
      return;
    }
    req.headers.host = normalizeHostHeader(req.headers.host ?? "");
    next();
  };
  return {
    name: "dispatch-dev-network-guard",
    enforce: "pre",
    configurePreviewServer(server) {
      server.middlewares.use(middleware);
    },
    configureServer(server) {
      server.middlewares.use(middleware);
      // Vite registers HMR before configureServer. Wrap (rather than prepend to)
      // its listeners: EventEmitter otherwise continues into HMR after a denial.
      const http = server.httpServer;
      if (http) {
        const upgrades = http.listeners("upgrade");
        http.removeAllListeners("upgrade");
        http.on("upgrade", (req, socket, head) => {
          const decision = check(req);
          if (decision !== "ok") {
            socket.end(
              `HTTP/1.1 ${decision.status} ${decision.status === 421 ? "Misdirected Request" : "Forbidden"}\r\nConnection: close\r\nContent-Length: 0\r\n\r\n`,
            );
            return;
          }
          req.headers.host = normalizeHostHeader(req.headers.host ?? "");
          for (const upgrade of upgrades) upgrade.call(http, req, socket, head);
        });
      }
    },
  };
}