@shimabell06/cloud-arch-icon-browser 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cli/server.js CHANGED
@@ -1,7 +1,12 @@
1
1
  import { readFile, readdir, realpath } from "node:fs/promises";
2
- import { createServer, } from "node:http";
2
+ import { createServer, request as httpRequest, } from "node:http";
3
3
  import { extname, resolve, sep } from "node:path";
4
+ import { createPowerPointBridge, } from "./powerpoint-bridge.js";
4
5
  const LOOPBACK_HOST = "127.0.0.1";
6
+ export const CANONICAL_PORT = 41731;
7
+ export const APP_INSTANCE_HEADER = "X-Cloud-Arch-Icon-Browser-Instance";
8
+ const APP_INSTANCE_HEADER_VALUE = "1";
9
+ const EXISTING_INSTANCE_PROBE_TIMEOUT_MS = 750;
5
10
  const CONTENT_SECURITY_POLICY = [
6
11
  "default-src 'self'",
7
12
  "script-src 'self'",
@@ -37,48 +42,69 @@ const MIME_TYPES = new Map([
37
42
  export async function startStaticServer(options) {
38
43
  const rootDirectory = await realpath(options.rootDirectory);
39
44
  const staticAssets = await loadStaticAssets(rootDirectory);
40
- let selectedPort = 0;
45
+ const powerPointBridge = options.powerPointBridge ?? createPowerPointBridge();
46
+ const requestedPort = options.port ?? CANONICAL_PORT;
47
+ let selectedPort = requestedPort;
41
48
  const server = createServer((request, response) => {
42
- try {
43
- handleRequest(request, response, staticAssets, selectedPort);
44
- }
45
- catch {
49
+ void handleRequest(request, response, staticAssets, powerPointBridge, selectedPort).catch(() => {
46
50
  if (!response.headersSent) {
47
51
  sendText(request, response, 500, "Internal Server Error\n");
48
52
  return;
49
53
  }
50
54
  response.destroy();
51
- }
55
+ });
52
56
  });
53
57
  server.on("clientError", (_error, socket) => {
54
58
  if (socket.writable) {
55
59
  socket.end("HTTP/1.1 400 Bad Request\r\nConnection: close\r\nContent-Length: 0\r\n\r\n");
56
60
  }
57
61
  });
58
- await listenOnAvailablePort(server);
62
+ try {
63
+ await listenOnPort(server, requestedPort);
64
+ }
65
+ catch (error) {
66
+ if (requestedPort > 0 && isAddressInUseError(error)) {
67
+ const reused = await probeExistingInstance(requestedPort);
68
+ if (reused) {
69
+ return {
70
+ port: requestedPort,
71
+ url: appUrl(requestedPort),
72
+ reused: true,
73
+ close: () => Promise.resolve(),
74
+ };
75
+ }
76
+ throw new Error(`Port ${requestedPort} is already in use by another process. Stop that process and run cloud-arch-icon-browser again.`, { cause: error });
77
+ }
78
+ throw error;
79
+ }
59
80
  const address = server.address();
60
81
  if (address === null || typeof address === "string") {
61
82
  await closeServer(server);
62
83
  throw new Error("Unable to determine the localhost server port.");
63
84
  }
64
85
  selectedPort = address.port;
65
- const url = `http://${LOOPBACK_HOST}:${selectedPort}/`;
86
+ const url = appUrl(selectedPort);
66
87
  let closePromise = null;
67
88
  return {
68
89
  port: selectedPort,
69
90
  url,
91
+ reused: false,
70
92
  close: () => {
71
93
  closePromise ??= closeServer(server);
72
94
  return closePromise;
73
95
  },
74
96
  };
75
97
  }
76
- function handleRequest(request, response, staticAssets, port) {
98
+ async function handleRequest(request, response, staticAssets, powerPointBridge, port) {
77
99
  applySecurityHeaders(response);
78
100
  if (!isExpectedHost(request.headers.host, port)) {
79
101
  sendText(request, response, 403, "Forbidden\n");
80
102
  return;
81
103
  }
104
+ if (powerPointBridge.matches(request.url)) {
105
+ await powerPointBridge.handle(request, response, port);
106
+ return;
107
+ }
82
108
  if (request.method !== "GET" && request.method !== "HEAD") {
83
109
  response.setHeader("Allow", "GET, HEAD");
84
110
  sendText(request, response, 405, "Method Not Allowed\n");
@@ -192,6 +218,7 @@ function cacheControlFor(relativePath) {
192
218
  return "no-cache";
193
219
  }
194
220
  function applySecurityHeaders(response) {
221
+ response.setHeader(APP_INSTANCE_HEADER, APP_INSTANCE_HEADER_VALUE);
195
222
  response.setHeader("Content-Security-Policy", CONTENT_SECURITY_POLICY);
196
223
  response.setHeader("Cross-Origin-Opener-Policy", "same-origin");
197
224
  response.setHeader("Permissions-Policy", permissionsPolicy());
@@ -224,7 +251,7 @@ function sendText(request, response, statusCode, body) {
224
251
  }
225
252
  response.end(body);
226
253
  }
227
- function listenOnAvailablePort(server) {
254
+ function listenOnPort(server, port) {
228
255
  return new Promise((resolvePromise, rejectPromise) => {
229
256
  const handleError = (error) => {
230
257
  server.off("listening", handleListening);
@@ -236,9 +263,45 @@ function listenOnAvailablePort(server) {
236
263
  };
237
264
  server.once("error", handleError);
238
265
  server.once("listening", handleListening);
239
- server.listen({ host: LOOPBACK_HOST, port: 0 });
266
+ server.listen({ host: LOOPBACK_HOST, port });
240
267
  });
241
268
  }
269
+ function probeExistingInstance(port) {
270
+ return new Promise((resolvePromise) => {
271
+ let settled = false;
272
+ const finish = (result) => {
273
+ if (settled)
274
+ return;
275
+ settled = true;
276
+ resolvePromise(result);
277
+ };
278
+ const clientRequest = httpRequest({
279
+ host: LOOPBACK_HOST,
280
+ port,
281
+ path: "/",
282
+ method: "HEAD",
283
+ headers: { Host: `${LOOPBACK_HOST}:${port}` },
284
+ }, (response) => {
285
+ const header = response.headers[APP_INSTANCE_HEADER.toLowerCase()];
286
+ response.resume();
287
+ finish(header === APP_INSTANCE_HEADER_VALUE);
288
+ });
289
+ clientRequest.setTimeout(EXISTING_INSTANCE_PROBE_TIMEOUT_MS, () => {
290
+ clientRequest.destroy();
291
+ finish(false);
292
+ });
293
+ clientRequest.on("error", () => finish(false));
294
+ clientRequest.end();
295
+ });
296
+ }
297
+ function isAddressInUseError(error) {
298
+ return (error instanceof Error &&
299
+ "code" in error &&
300
+ error.code === "EADDRINUSE");
301
+ }
302
+ function appUrl(port) {
303
+ return `http://${LOOPBACK_HOST}:${port}/`;
304
+ }
242
305
  function closeServer(server) {
243
306
  if (!server.listening)
244
307
  return Promise.resolve();