Files
meteor/packages/dynamic-import/server.js
Nathan Muir dd66cdf24a dynamic-import: add Allow & Content-Length headers to responses
[RFC 7231 OPTIONS](https://tools.ietf.org/html/rfc7231#section-4.3.7)

> A server generating a successful response to OPTIONS SHOULD send any header fields that might indicate optional features implemented by the server and applicable to the target resource (e.g., Allow)

> A server MUST generate a Content-Length field with a value of "0" if no payload body is to be sent in the response.

[RFC 7231 405 Method Not Allowed](https://tools.ietf.org/html/rfc7231#section-6.5.5)

> The origin server MUST generate an Allow header field in a 405 response containing a list of the target resource's currently supported methods
2020-11-23 12:27:26 +13:00

217 lines
5.8 KiB
JavaScript

"use strict";
const assert = require("assert");
const { readFileSync } = require("fs");
const {
join: pathJoin,
normalize: pathNormalize,
} = require("path");
const { fetchURL } = require("./common.js");
const { Meteor } = require("meteor/meteor");
const hasOwn = Object.prototype.hasOwnProperty;
require("./security.js");
const client = require("./client.js");
Meteor.startup(() => {
if (! Package.webapp) {
// If the webapp package is not in use, there's no way for the
// dynamic-import package to fetch dynamic modules, so we should
// abandon the rest of the logic in this module.
//
// If api.use("webapp") appeared in dynamic-import/package.js, then
// Package.webapp would always be defined here, of course, but that
// would be a bad idea, because the dynamic-import package should not
// single-handedly force a dependency on webapp if the program does
// not otherwise need a web server (e.g., when the program is an
// isopacket or build plugin instead of a web application).
//
// Note that the client.js module (imported above) still defines
// Module.prototype.dynamicImport, which will work as long as no
// modules need to be fetched.
return;
}
Object.keys(dynamicImportInfo).forEach(setUpPlatform);
Package.webapp.WebAppInternals.meteorInternalHandlers.use(
fetchURL,
middleware
);
});
function setUpPlatform(platform) {
const info = dynamicImportInfo[platform];
if (info.dynamicRoot) {
info.dynamicRoot = pathNormalize(info.dynamicRoot);
}
if (platform === "server") {
client.setSecretKey(info.key = randomId(40));
}
}
function randomId(n) {
let s = "";
while (s.length < n) {
s += Math.random().toString(36).slice(2);
}
return s.slice(0, n);
}
function middleware(request, response) {
// Allow dynamic import() requests from any origin.
response.setHeader("Access-Control-Allow-Origin", "*");
if (request.method === "OPTIONS") {
const acrh = request.headers["access-control-request-headers"];
response.setHeader('Allow', 'OPTIONS, POST');
response.setHeader('Content-Length', '0');
response.setHeader(
"Access-Control-Allow-Headers",
typeof acrh === "string" ? acrh : "*"
);
response.setHeader("Access-Control-Allow-Methods", "POST");
response.end();
} else if (request.method === "POST") {
const chunks = [];
request.on("data", chunk => chunks.push(chunk));
request.on("end", () => {
try {
const tree = JSON.stringify(readTree(
JSON.parse(Buffer.concat(chunks)),
getPlatform(request)
), null, 2);
response.writeHead(200, {
"Content-Type": "application/json"
});
response.end(tree);
} catch (e) {
response.writeHead(400, {
"Content-Type": "application/json"
});
response.end(JSON.stringify(
Meteor.isDevelopment && e.message || "bad request"
));
}
});
} else {
const body = `method ${request.method} not allowed`;
response.writeHead(405, {
Allow: "OPTIONS, POST",
'Content-Length': Buffer.byteLength(body),
"Cache-Control": "no-cache"
});
response.end(body);
}
}
function getPlatform(request) {
// If the __dynamicImport request includes a secret key, and it matches
// dynamicImportInfo[platform].key, use platform instead of the default
// platform, web.browser.
const secretKey = request.query.key;
if (typeof secretKey === "string") {
for (const p of Object.keys(dynamicImportInfo)) {
if (secretKey === dynamicImportInfo[p].key) {
return p;
}
}
}
return Package.webapp.WebApp.categorizeRequest(request).arch;
}
function readTree(tree, platform) {
const pathParts = [];
function walk(node) {
if (! node) {
return null;
}
if (typeof node !== "object") {
return read(pathParts, platform);
}
let empty = true;
Object.keys(node).forEach(name => {
pathParts.push(name);
const result = walk(node[name]);
if (result === null) {
// If the read function returns null, omit this module from the
// resulting tree.
delete node[name];
} else {
node[name] = result;
empty = false;
}
assert.strictEqual(pathParts.pop(), name);
});
if (empty) {
// If every recursive call to walk(node[name]) returned null,
// remove this node from the resulting tree by returning null.
return null;
}
return node;
}
return walk(tree);
}
function read(pathParts, platform) {
const { dynamicRoot } = dynamicImportInfo[platform];
const absPath = pathNormalize(pathJoin(
dynamicRoot,
pathJoin(...pathParts).replace(/:/g, "_")
));
if (! absPath.startsWith(dynamicRoot)) {
console.error("bad dynamic import path:", absPath);
return null;
}
const cache = getCache(platform);
if (hasOwn.call(cache, absPath)) {
return cache[absPath];
}
try {
return cache[absPath] = readFileSync(absPath, "utf8");
} catch (e) {
console.error(e.stack || e);
return null;
}
}
const cachesByPlatform = Object.create(null);
function getCache(platform) {
return hasOwn.call(cachesByPlatform, platform)
? cachesByPlatform[platform]
: cachesByPlatform[platform] = Object.create(null);
}
const { onMessage } = require("meteor/inter-process-messaging");
onMessage("client-refresh", () => {
// The caches for the web.browser[.legacy] platforms need to be
// discarded whenever a client-only refresh occurs, so the new client
// bundle does not fetch stale module data from dynamic import(). This
// message is sent by tools/runners/run-app.js and also consumed by the
// autoupdate package.
Object.keys(cachesByPlatform).forEach(platform => {
delete cachesByPlatform[platform];
});
});