mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
[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
217 lines
5.8 KiB
JavaScript
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];
|
|
});
|
|
});
|