@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/CHANGELOG.md +18 -0
- package/README.md +56 -78
- package/cli/powerpoint-bridge.js +406 -0
- package/cli/server.js +75 -12
- package/dist/assets/index-CF_evZHB.js +9 -0
- package/dist/assets/index-jxRCnk3-.css +2 -0
- package/dist/index.html +2 -2
- package/package.json +2 -1
- package/dist/assets/index-CE34ADfR.css +0 -2
- package/dist/assets/index-wz5PJNXz.js +0 -9
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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();
|