@electron-memory/monitor 0.2.1 → 0.2.3

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/dist/index.js CHANGED
@@ -1114,6 +1114,7 @@ var path3 = __toESM(require("path"));
1114
1114
  var import_electron2 = require("electron");
1115
1115
  var import_promises = require("fs/promises");
1116
1116
  var path2 = __toESM(require("path"));
1117
+ var import_node_url = require("url");
1117
1118
  var SCHEME = "emm-dashboard";
1118
1119
  var privilegedRegistered = false;
1119
1120
  var handlerRegistered = false;
@@ -1142,8 +1143,60 @@ function isPathInsideRoot(filePath, root) {
1142
1143
  if (f === r) {
1143
1144
  return true;
1144
1145
  }
1145
- const sep2 = r.endsWith(path2.sep) ? r : `${r}${path2.sep}`;
1146
- return f.startsWith(sep2);
1146
+ const rel = path2.relative(r, f);
1147
+ if (!rel || rel.startsWith("..") || path2.isAbsolute(rel)) {
1148
+ return false;
1149
+ }
1150
+ return true;
1151
+ }
1152
+ function urlToUiRelativePath(requestUrl) {
1153
+ let u;
1154
+ try {
1155
+ u = new URL(requestUrl);
1156
+ } catch {
1157
+ return "";
1158
+ }
1159
+ let p = "";
1160
+ try {
1161
+ p = decodeURIComponent(u.pathname || "");
1162
+ } catch {
1163
+ p = u.pathname || "";
1164
+ }
1165
+ p = p.replace(/^\/+/, "");
1166
+ const host = (u.hostname || "").toLowerCase();
1167
+ if (p.includes("..")) {
1168
+ return "";
1169
+ }
1170
+ if (host === "electron" || host === "") {
1171
+ return p || "index.html";
1172
+ }
1173
+ if (p) {
1174
+ return `${host}/${p}`.replace(/\\/g, "/");
1175
+ }
1176
+ if (host.includes(".")) {
1177
+ return host;
1178
+ }
1179
+ return "index.html";
1180
+ }
1181
+ function bufferToResponseBody(buf) {
1182
+ return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
1183
+ }
1184
+ function looksLikeHtml(buf) {
1185
+ let i = 0;
1186
+ if (buf.length >= 3 && buf[0] === 239 && buf[1] === 187 && buf[2] === 191) {
1187
+ i = 3;
1188
+ }
1189
+ while (i < buf.length && (buf[i] === 32 || buf[i] === 9 || buf[i] === 10 || buf[i] === 13)) {
1190
+ i += 1;
1191
+ }
1192
+ return i < buf.length && buf[i] === 60;
1193
+ }
1194
+ function corsHeaders() {
1195
+ return {
1196
+ "Access-Control-Allow-Origin": "*",
1197
+ "Access-Control-Allow-Methods": "GET, OPTIONS",
1198
+ "Access-Control-Allow-Headers": "*"
1199
+ };
1147
1200
  }
1148
1201
  function registerDashboardSchemePrivileged() {
1149
1202
  if (privilegedRegistered) {
@@ -1175,29 +1228,55 @@ function ensureDashboardProtocolHandler(uiRoot) {
1175
1228
  handlerRegistered = true;
1176
1229
  const base = path2.resolve(uiRoot);
1177
1230
  import_electron2.protocol.handle(SCHEME, async (request) => {
1231
+ if (request.method === "OPTIONS") {
1232
+ return new Response(null, { status: 204, headers: corsHeaders() });
1233
+ }
1178
1234
  try {
1179
- const { pathname } = new URL(request.url);
1180
- let rel = pathname.startsWith("/") ? pathname.slice(1) : pathname;
1181
- if (!rel) {
1182
- rel = "index.html";
1235
+ const rel = urlToUiRelativePath(request.url);
1236
+ if (!rel || rel.includes("..")) {
1237
+ return new Response("Bad path", { status: 400, headers: corsHeaders() });
1183
1238
  }
1184
- const filePath = path2.resolve(path2.join(base, rel));
1239
+ const filePath = path2.resolve(path2.join(base, ...rel.split("/")));
1185
1240
  if (!isPathInsideRoot(filePath, base)) {
1186
- return new Response("Forbidden", { status: 403 });
1241
+ return new Response("Forbidden", { status: 403, headers: corsHeaders() });
1242
+ }
1243
+ const fileUrl = (0, import_node_url.pathToFileURL)(filePath).href;
1244
+ let upstream;
1245
+ try {
1246
+ upstream = await import_electron2.net.fetch(fileUrl);
1247
+ } catch {
1248
+ upstream = new Response(null, { status: 599 });
1249
+ }
1250
+ let buf;
1251
+ if (!upstream.ok) {
1252
+ buf = await (0, import_promises.readFile)(filePath);
1253
+ } else {
1254
+ const ab = await upstream.arrayBuffer();
1255
+ buf = Buffer.from(ab);
1187
1256
  }
1188
- const body = await (0, import_promises.readFile)(filePath);
1189
1257
  const ext = path2.extname(filePath).toLowerCase();
1258
+ if (ext === ".html" && !looksLikeHtml(buf)) {
1259
+ console.error(
1260
+ "[@electron-memory/monitor] emm-dashboard: not HTML at",
1261
+ filePath,
1262
+ "url=",
1263
+ request.url
1264
+ );
1265
+ return new Response("Invalid dashboard HTML", { status: 500, headers: corsHeaders() });
1266
+ }
1190
1267
  const mime = MIME_BY_EXT[ext] || "application/octet-stream";
1191
- return new Response(body, {
1268
+ return new Response(bufferToResponseBody(buf), {
1192
1269
  status: 200,
1193
1270
  headers: {
1194
1271
  "Content-Type": mime,
1195
- "Cache-Control": "no-store"
1272
+ "Cache-Control": "no-store",
1273
+ ...corsHeaders()
1196
1274
  }
1197
1275
  });
1198
1276
  } catch (err) {
1199
1277
  const msg = err instanceof Error ? err.message : String(err);
1200
- return new Response(msg, { status: 404 });
1278
+ console.error("[@electron-memory/monitor] emm-dashboard:", request.url, err);
1279
+ return new Response(msg, { status: 404, headers: corsHeaders() });
1201
1280
  }
1202
1281
  });
1203
1282
  }