mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
1498 lines
49 KiB
JavaScript
1498 lines
49 KiB
JavaScript
import assert from 'assert';
|
|
import { readFileSync, chmodSync, chownSync } from 'fs';
|
|
import { createServer } from 'http';
|
|
import { userInfo } from 'os';
|
|
import { join as pathJoin, dirname as pathDirname } from 'path';
|
|
import { parse as parseUrl } from 'url';
|
|
import { createHash } from 'crypto';
|
|
import { connect } from './connect.js';
|
|
import compress from 'compression';
|
|
import cookieParser from 'cookie-parser';
|
|
import qs from 'qs';
|
|
import parseRequest from 'parseurl';
|
|
import basicAuth from 'basic-auth-connect';
|
|
import { lookup as lookupUserAgent } from 'useragent';
|
|
import { isModern } from 'meteor/modern-browsers';
|
|
import send from 'send';
|
|
import {
|
|
removeExistingSocketFile,
|
|
registerSocketFileCleanup,
|
|
} from './socket_file.js';
|
|
import cluster from 'cluster';
|
|
import whomst from '@vlasky/whomst';
|
|
|
|
var SHORT_SOCKET_TIMEOUT = 5 * 1000;
|
|
var LONG_SOCKET_TIMEOUT = 120 * 1000;
|
|
|
|
export const WebApp = {};
|
|
export const WebAppInternals = {};
|
|
|
|
const hasOwn = Object.prototype.hasOwnProperty;
|
|
|
|
// backwards compat to 2.0 of connect
|
|
connect.basicAuth = basicAuth;
|
|
|
|
WebAppInternals.NpmModules = {
|
|
connect: {
|
|
version: Npm.require('connect/package.json').version,
|
|
module: connect,
|
|
},
|
|
};
|
|
|
|
// Though we might prefer to use web.browser (modern) as the default
|
|
// architecture, safety requires a more compatible defaultArch.
|
|
WebApp.defaultArch = 'web.browser.legacy';
|
|
|
|
// XXX maps archs to manifests
|
|
WebApp.clientPrograms = {};
|
|
|
|
// XXX maps archs to program path on filesystem
|
|
var archPath = {};
|
|
|
|
var bundledJsCssUrlRewriteHook = function(url) {
|
|
var bundledPrefix = __meteor_runtime_config__.ROOT_URL_PATH_PREFIX || '';
|
|
return bundledPrefix + url;
|
|
};
|
|
|
|
var sha1 = function(contents) {
|
|
var hash = createHash('sha1');
|
|
hash.update(contents);
|
|
return hash.digest('hex');
|
|
};
|
|
|
|
function shouldCompress(req, res) {
|
|
if (req.headers['x-no-compression']) {
|
|
// don't compress responses with this request header
|
|
return false;
|
|
}
|
|
|
|
// fallback to standard filter function
|
|
return compress.filter(req, res);
|
|
}
|
|
|
|
// #BrowserIdentification
|
|
//
|
|
// We have multiple places that want to identify the browser: the
|
|
// unsupported browser page, the appcache package, and, eventually
|
|
// delivering browser polyfills only as needed.
|
|
//
|
|
// To avoid detecting the browser in multiple places ad-hoc, we create a
|
|
// Meteor "browser" object. It uses but does not expose the npm
|
|
// useragent module (we could choose a different mechanism to identify
|
|
// the browser in the future if we wanted to). The browser object
|
|
// contains
|
|
//
|
|
// * `name`: the name of the browser in camel case
|
|
// * `major`, `minor`, `patch`: integers describing the browser version
|
|
//
|
|
// Also here is an early version of a Meteor `request` object, intended
|
|
// to be a high-level description of the request without exposing
|
|
// details of connect's low-level `req`. Currently it contains:
|
|
//
|
|
// * `browser`: browser identification object described above
|
|
// * `url`: parsed url, including parsed query params
|
|
//
|
|
// As a temporary hack there is a `categorizeRequest` function on WebApp which
|
|
// converts a connect `req` to a Meteor `request`. This can go away once smart
|
|
// packages such as appcache are being passed a `request` object directly when
|
|
// they serve content.
|
|
//
|
|
// This allows `request` to be used uniformly: it is passed to the html
|
|
// attributes hook, and the appcache package can use it when deciding
|
|
// whether to generate a 404 for the manifest.
|
|
//
|
|
// Real routing / server side rendering will probably refactor this
|
|
// heavily.
|
|
|
|
// e.g. "Mobile Safari" => "mobileSafari"
|
|
var camelCase = function(name) {
|
|
var parts = name.split(' ');
|
|
parts[0] = parts[0].toLowerCase();
|
|
for (var i = 1; i < parts.length; ++i) {
|
|
parts[i] = parts[i].charAt(0).toUpperCase() + parts[i].substr(1);
|
|
}
|
|
return parts.join('');
|
|
};
|
|
|
|
var identifyBrowser = function(userAgentString) {
|
|
var userAgent = lookupUserAgent(userAgentString);
|
|
return {
|
|
name: camelCase(userAgent.family),
|
|
major: +userAgent.major,
|
|
minor: +userAgent.minor,
|
|
patch: +userAgent.patch,
|
|
};
|
|
};
|
|
|
|
// XXX Refactor as part of implementing real routing.
|
|
WebAppInternals.identifyBrowser = identifyBrowser;
|
|
|
|
WebApp.categorizeRequest = function(req) {
|
|
if (req.browser && req.arch && typeof req.modern === 'boolean') {
|
|
// Already categorized.
|
|
return req;
|
|
}
|
|
|
|
const browser = identifyBrowser(req.headers['user-agent']);
|
|
const modern = isModern(browser);
|
|
const path =
|
|
typeof req.pathname === 'string'
|
|
? req.pathname
|
|
: parseRequest(req).pathname;
|
|
|
|
const categorized = {
|
|
browser,
|
|
modern,
|
|
path,
|
|
arch: WebApp.defaultArch,
|
|
url: parseUrl(req.url, true),
|
|
dynamicHead: req.dynamicHead,
|
|
dynamicBody: req.dynamicBody,
|
|
headers: req.headers,
|
|
cookies: req.cookies,
|
|
};
|
|
|
|
const pathParts = path.split('/');
|
|
const archKey = pathParts[1];
|
|
|
|
if (archKey.startsWith('__')) {
|
|
const archCleaned = 'web.' + archKey.slice(2);
|
|
if (hasOwn.call(WebApp.clientPrograms, archCleaned)) {
|
|
pathParts.splice(1, 1); // Remove the archKey part.
|
|
return Object.assign(categorized, {
|
|
arch: archCleaned,
|
|
path: pathParts.join('/'),
|
|
});
|
|
}
|
|
}
|
|
|
|
// TODO Perhaps one day we could infer Cordova clients here, so that we
|
|
// wouldn't have to use prefixed "/__cordova/..." URLs.
|
|
const preferredArchOrder = isModern(browser)
|
|
? ['web.browser', 'web.browser.legacy']
|
|
: ['web.browser.legacy', 'web.browser'];
|
|
|
|
for (const arch of preferredArchOrder) {
|
|
// If our preferred arch is not available, it's better to use another
|
|
// client arch that is available than to guarantee the site won't work
|
|
// by returning an unknown arch. For example, if web.browser.legacy is
|
|
// excluded using the --exclude-archs command-line option, legacy
|
|
// clients are better off receiving web.browser (which might actually
|
|
// work) than receiving an HTTP 404 response. If none of the archs in
|
|
// preferredArchOrder are defined, only then should we send a 404.
|
|
if (hasOwn.call(WebApp.clientPrograms, arch)) {
|
|
return Object.assign(categorized, { arch });
|
|
}
|
|
}
|
|
|
|
return categorized;
|
|
};
|
|
|
|
// HTML attribute hooks: functions to be called to determine any attributes to
|
|
// be added to the '<html>' tag. Each function is passed a 'request' object (see
|
|
// #BrowserIdentification) and should return null or object.
|
|
var htmlAttributeHooks = [];
|
|
var getHtmlAttributes = function(request) {
|
|
var combinedAttributes = {};
|
|
_.each(htmlAttributeHooks || [], function(hook) {
|
|
var attributes = hook(request);
|
|
if (attributes === null) return;
|
|
if (typeof attributes !== 'object')
|
|
throw Error('HTML attribute hook must return null or object');
|
|
_.extend(combinedAttributes, attributes);
|
|
});
|
|
return combinedAttributes;
|
|
};
|
|
WebApp.addHtmlAttributeHook = function(hook) {
|
|
htmlAttributeHooks.push(hook);
|
|
};
|
|
|
|
// Serve app HTML for this URL?
|
|
var appUrl = function(url) {
|
|
if (url === '/favicon.ico' || url === '/robots.txt') return false;
|
|
|
|
// NOTE: app.manifest is not a web standard like favicon.ico and
|
|
// robots.txt. It is a file name we have chosen to use for HTML5
|
|
// appcache URLs. It is included here to prevent using an appcache
|
|
// then removing it from poisoning an app permanently. Eventually,
|
|
// once we have server side routing, this won't be needed as
|
|
// unknown URLs with return a 404 automatically.
|
|
if (url === '/app.manifest') return false;
|
|
|
|
// Avoid serving app HTML for declared routes such as /sockjs/.
|
|
if (RoutePolicy.classify(url)) return false;
|
|
|
|
// we currently return app HTML on all URLs by default
|
|
return true;
|
|
};
|
|
|
|
// We need to calculate the client hash after all packages have loaded
|
|
// to give them a chance to populate __meteor_runtime_config__.
|
|
//
|
|
// Calculating the hash during startup means that packages can only
|
|
// populate __meteor_runtime_config__ during load, not during startup.
|
|
//
|
|
// Calculating instead it at the beginning of main after all startup
|
|
// hooks had run would allow packages to also populate
|
|
// __meteor_runtime_config__ during startup, but that's too late for
|
|
// autoupdate because it needs to have the client hash at startup to
|
|
// insert the auto update version itself into
|
|
// __meteor_runtime_config__ to get it to the client.
|
|
//
|
|
// An alternative would be to give autoupdate a "post-start,
|
|
// pre-listen" hook to allow it to insert the auto update version at
|
|
// the right moment.
|
|
|
|
Meteor.startup(function() {
|
|
function getter(key) {
|
|
return function(arch) {
|
|
arch = arch || WebApp.defaultArch;
|
|
const program = WebApp.clientPrograms[arch];
|
|
const value = program && program[key];
|
|
// If this is the first time we have calculated this hash,
|
|
// program[key] will be a thunk (lazy function with no parameters)
|
|
// that we should call to do the actual computation.
|
|
return typeof value === 'function' ? (program[key] = value()) : value;
|
|
};
|
|
}
|
|
|
|
WebApp.calculateClientHash = WebApp.clientHash = getter('version');
|
|
WebApp.calculateClientHashRefreshable = getter('versionRefreshable');
|
|
WebApp.calculateClientHashNonRefreshable = getter('versionNonRefreshable');
|
|
WebApp.calculateClientHashReplaceable = getter('versionReplaceable');
|
|
WebApp.getRefreshableAssets = getter('refreshableAssets');
|
|
});
|
|
|
|
// When we have a request pending, we want the socket timeout to be long, to
|
|
// give ourselves a while to serve it, and to allow sockjs long polls to
|
|
// complete. On the other hand, we want to close idle sockets relatively
|
|
// quickly, so that we can shut down relatively promptly but cleanly, without
|
|
// cutting off anyone's response.
|
|
WebApp._timeoutAdjustmentRequestCallback = function(req, res) {
|
|
// this is really just req.socket.setTimeout(LONG_SOCKET_TIMEOUT);
|
|
req.setTimeout(LONG_SOCKET_TIMEOUT);
|
|
// Insert our new finish listener to run BEFORE the existing one which removes
|
|
// the response from the socket.
|
|
var finishListeners = res.listeners('finish');
|
|
// XXX Apparently in Node 0.12 this event was called 'prefinish'.
|
|
// https://github.com/joyent/node/commit/7c9b6070
|
|
// But it has switched back to 'finish' in Node v4:
|
|
// https://github.com/nodejs/node/pull/1411
|
|
res.removeAllListeners('finish');
|
|
res.on('finish', function() {
|
|
res.setTimeout(SHORT_SOCKET_TIMEOUT);
|
|
});
|
|
_.each(finishListeners, function(l) {
|
|
res.on('finish', l);
|
|
});
|
|
};
|
|
|
|
// Will be updated by main before we listen.
|
|
// Map from client arch to boilerplate object.
|
|
// Boilerplate object has:
|
|
// - func: XXX
|
|
// - baseData: XXX
|
|
var boilerplateByArch = {};
|
|
|
|
// Register a callback function that can selectively modify boilerplate
|
|
// data given arguments (request, data, arch). The key should be a unique
|
|
// identifier, to prevent accumulating duplicate callbacks from the same
|
|
// call site over time. Callbacks will be called in the order they were
|
|
// registered. A callback should return false if it did not make any
|
|
// changes affecting the boilerplate. Passing null deletes the callback.
|
|
// Any previous callback registered for this key will be returned.
|
|
const boilerplateDataCallbacks = Object.create(null);
|
|
WebAppInternals.registerBoilerplateDataCallback = function(key, callback) {
|
|
const previousCallback = boilerplateDataCallbacks[key];
|
|
|
|
if (typeof callback === 'function') {
|
|
boilerplateDataCallbacks[key] = callback;
|
|
} else {
|
|
assert.strictEqual(callback, null);
|
|
delete boilerplateDataCallbacks[key];
|
|
}
|
|
|
|
// Return the previous callback in case the new callback needs to call
|
|
// it; for example, when the new callback is a wrapper for the old.
|
|
return previousCallback || null;
|
|
};
|
|
|
|
// Given a request (as returned from `categorizeRequest`), return the
|
|
// boilerplate HTML to serve for that request.
|
|
//
|
|
// If a previous connect middleware has rendered content for the head or body,
|
|
// returns the boilerplate with that content patched in otherwise
|
|
// memoizes on HTML attributes (used by, eg, appcache) and whether inline
|
|
// scripts are currently allowed.
|
|
// XXX so far this function is always called with arch === 'web.browser'
|
|
function getBoilerplate(request, arch) {
|
|
return getBoilerplateAsync(request, arch).await();
|
|
}
|
|
|
|
/**
|
|
* @summary Takes a runtime configuration object and
|
|
* returns an encoded runtime string.
|
|
* @locus Server
|
|
* @param {Object} rtimeConfig
|
|
* @returns {String}
|
|
*/
|
|
WebApp.encodeRuntimeConfig = function(rtimeConfig) {
|
|
return JSON.stringify(encodeURIComponent(JSON.stringify(rtimeConfig)));
|
|
};
|
|
|
|
/**
|
|
* @summary Takes an encoded runtime string and returns
|
|
* a runtime configuration object.
|
|
* @locus Server
|
|
* @param {String} rtimeConfigString
|
|
* @returns {Object}
|
|
*/
|
|
WebApp.decodeRuntimeConfig = function(rtimeConfigStr) {
|
|
return JSON.parse(decodeURIComponent(JSON.parse(rtimeConfigStr)));
|
|
};
|
|
|
|
const runtimeConfig = {
|
|
// hooks will contain the callback functions
|
|
// set by the caller to addRuntimeConfigHook
|
|
hooks: new Hook(),
|
|
// updateHooks will contain the callback functions
|
|
// set by the caller to addUpdatedNotifyHook
|
|
updateHooks: new Hook(),
|
|
// isUpdatedByArch is an object containing fields for each arch
|
|
// that this server supports.
|
|
// - Each field will be true when the server updates the runtimeConfig for that arch.
|
|
// - When the hook callback is called the update field in the callback object will be
|
|
// set to isUpdatedByArch[arch].
|
|
// = isUpdatedyByArch[arch] is reset to false after the callback.
|
|
// This enables the caller to cache data efficiently so they do not need to
|
|
// decode & update data on every callback when the runtimeConfig is not changing.
|
|
isUpdatedByArch: {},
|
|
};
|
|
|
|
/**
|
|
* @name addRuntimeConfigHookCallback(options)
|
|
* @locus Server
|
|
* @isprototype true
|
|
* @summary Callback for `addRuntimeConfigHook`.
|
|
*
|
|
* If the handler returns a _falsy_ value the hook will not
|
|
* modify the runtime configuration.
|
|
*
|
|
* If the handler returns a _String_ the hook will substitute
|
|
* the string for the encoded configuration string.
|
|
*
|
|
* **Warning:** the hook does not check the return value at all it is
|
|
* the responsibility of the caller to get the formatting correct using
|
|
* the helper functions.
|
|
*
|
|
* `addRuntimeConfigHookCallback` takes only one `Object` argument
|
|
* with the following fields:
|
|
* @param {Object} options
|
|
* @param {String} options.arch The architecture of the client
|
|
* requesting a new runtime configuration. This can be one of
|
|
* `web.browser`, `web.browser.legacy` or `web.cordova`.
|
|
* @param {Object} options.request
|
|
* A NodeJs [IncomingMessage](https://nodejs.org/api/http.html#http_class_http_incomingmessage)
|
|
* https://nodejs.org/api/http.html#http_class_http_incomingmessage
|
|
* `Object` that can be used to get information about the incoming request.
|
|
* @param {String} options.encodedCurrentConfig The current configuration object
|
|
* encoded as a string for inclusion in the root html.
|
|
* @param {Boolean} options.updated `true` if the config for this architecture
|
|
* has been updated since last called, otherwise `false`. This flag can be used
|
|
* to cache the decoding/encoding for each architecture.
|
|
*/
|
|
|
|
/**
|
|
* @summary Hook that calls back when the meteor runtime configuration,
|
|
* `__meteor_runtime_config__` is being sent to any client.
|
|
*
|
|
* **returns**: <small>_Object_</small> `{ stop: function, callback: function }`
|
|
* - `stop` <small>_Function_</small> Call `stop()` to stop getting callbacks.
|
|
* - `callback` <small>_Function_</small> The passed in `callback`.
|
|
* @locus Server
|
|
* @param {addRuntimeConfigHookCallback} callback
|
|
* See `addRuntimeConfigHookCallback` description.
|
|
* @returns {Object} {{ stop: function, callback: function }}
|
|
* Call the returned `stop()` to stop getting callbacks.
|
|
* The passed in `callback` is returned also.
|
|
*/
|
|
WebApp.addRuntimeConfigHook = function(callback) {
|
|
return runtimeConfig.hooks.register(callback);
|
|
};
|
|
|
|
function getBoilerplateAsync(request, arch) {
|
|
let boilerplate = boilerplateByArch[arch];
|
|
runtimeConfig.hooks.forEach(hook => {
|
|
const meteorRuntimeConfig = hook({
|
|
arch,
|
|
request,
|
|
encodedCurrentConfig: boilerplate.baseData.meteorRuntimeConfig,
|
|
updated: runtimeConfig.isUpdatedByArch[arch],
|
|
});
|
|
if (!meteorRuntimeConfig) return true;
|
|
boilerplate.baseData = Object.assign({}, boilerplate.baseData, {
|
|
meteorRuntimeConfig,
|
|
});
|
|
return true;
|
|
});
|
|
runtimeConfig.isUpdatedByArch[arch] = false;
|
|
const data = Object.assign(
|
|
{},
|
|
boilerplate.baseData,
|
|
{
|
|
htmlAttributes: getHtmlAttributes(request),
|
|
},
|
|
_.pick(request, 'dynamicHead', 'dynamicBody')
|
|
);
|
|
|
|
let madeChanges = false;
|
|
let promise = Promise.resolve();
|
|
|
|
Object.keys(boilerplateDataCallbacks).forEach(key => {
|
|
promise = promise
|
|
.then(() => {
|
|
const callback = boilerplateDataCallbacks[key];
|
|
return callback(request, data, arch);
|
|
})
|
|
.then(result => {
|
|
// Callbacks should return false if they did not make any changes.
|
|
if (result !== false) {
|
|
madeChanges = true;
|
|
}
|
|
});
|
|
});
|
|
|
|
return promise.then(() => ({
|
|
stream: boilerplate.toHTMLStream(data),
|
|
statusCode: data.statusCode,
|
|
headers: data.headers,
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* @name addUpdatedNotifyHookCallback(options)
|
|
* @summary callback handler for `addupdatedNotifyHook`
|
|
* @isprototype true
|
|
* @locus Server
|
|
* @param {Object} options
|
|
* @param {String} options.arch The architecture that is being updated.
|
|
* This can be one of `web.browser`, `web.browser.legacy` or `web.cordova`.
|
|
* @param {Object} options.manifest The new updated manifest object for
|
|
* this `arch`.
|
|
* @param {Object} options.runtimeConfig The new updated configuration
|
|
* object for this `arch`.
|
|
*/
|
|
|
|
/**
|
|
* @summary Hook that runs when the meteor runtime configuration
|
|
* is updated. Typically the configuration only changes during development mode.
|
|
* @locus Server
|
|
* @param {addUpdatedNotifyHookCallback} handler
|
|
* The `handler` is called on every change to an `arch` runtime configuration.
|
|
* See `addUpdatedNotifyHookCallback`.
|
|
* @returns {Object} {{ stop: function, callback: function }}
|
|
*/
|
|
WebApp.addUpdatedNotifyHook = function(handler) {
|
|
return runtimeConfig.updateHooks.register(handler);
|
|
};
|
|
|
|
WebAppInternals.generateBoilerplateInstance = function(
|
|
arch,
|
|
manifest,
|
|
additionalOptions
|
|
) {
|
|
additionalOptions = additionalOptions || {};
|
|
|
|
runtimeConfig.isUpdatedByArch[arch] = true;
|
|
const rtimeConfig = {
|
|
...__meteor_runtime_config__,
|
|
...(additionalOptions.runtimeConfigOverrides || {}),
|
|
};
|
|
runtimeConfig.updateHooks.forEach(cb => {
|
|
cb({ arch, manifest, runtimeConfig: rtimeConfig });
|
|
return true;
|
|
});
|
|
|
|
const meteorRuntimeConfig = JSON.stringify(
|
|
encodeURIComponent(JSON.stringify(rtimeConfig))
|
|
);
|
|
|
|
return new Boilerplate(
|
|
arch,
|
|
manifest,
|
|
Object.assign(
|
|
{
|
|
pathMapper(itemPath) {
|
|
return pathJoin(archPath[arch], itemPath);
|
|
},
|
|
baseDataExtension: {
|
|
additionalStaticJs: _.map(additionalStaticJs || [], function(
|
|
contents,
|
|
pathname
|
|
) {
|
|
return {
|
|
pathname: pathname,
|
|
contents: contents,
|
|
};
|
|
}),
|
|
// Convert to a JSON string, then get rid of most weird characters, then
|
|
// wrap in double quotes. (The outermost JSON.stringify really ought to
|
|
// just be "wrap in double quotes" but we use it to be safe.) This might
|
|
// end up inside a <script> tag so we need to be careful to not include
|
|
// "</script>", but normal {{spacebars}} escaping escapes too much! See
|
|
// https://github.com/meteor/meteor/issues/3730
|
|
meteorRuntimeConfig,
|
|
meteorRuntimeHash: sha1(meteorRuntimeConfig),
|
|
rootUrlPathPrefix:
|
|
__meteor_runtime_config__.ROOT_URL_PATH_PREFIX || '',
|
|
bundledJsCssUrlRewriteHook: bundledJsCssUrlRewriteHook,
|
|
sriMode: sriMode,
|
|
inlineScriptsAllowed: WebAppInternals.inlineScriptsAllowed(),
|
|
inline: additionalOptions.inline,
|
|
},
|
|
},
|
|
additionalOptions
|
|
)
|
|
);
|
|
};
|
|
|
|
// A mapping from url path to architecture (e.g. "web.browser") to static
|
|
// file information with the following fields:
|
|
// - type: the type of file to be served
|
|
// - cacheable: optionally, whether the file should be cached or not
|
|
// - sourceMapUrl: optionally, the url of the source map
|
|
//
|
|
// Info also contains one of the following:
|
|
// - content: the stringified content that should be served at this path
|
|
// - absolutePath: the absolute path on disk to the file
|
|
|
|
// Serve static files from the manifest or added with
|
|
// `addStaticJs`. Exported for tests.
|
|
WebAppInternals.staticFilesMiddleware = async function(
|
|
staticFilesByArch,
|
|
req,
|
|
res,
|
|
next
|
|
) {
|
|
var pathname = parseRequest(req).pathname;
|
|
try {
|
|
pathname = decodeURIComponent(pathname);
|
|
} catch (e) {
|
|
next();
|
|
return;
|
|
}
|
|
|
|
var serveStaticJs = function(s) {
|
|
if (
|
|
req.method === 'GET' ||
|
|
req.method === 'HEAD' ||
|
|
Meteor.settings.packages?.webapp?.alwaysReturnContent
|
|
) {
|
|
res.writeHead(200, {
|
|
'Content-type': 'application/javascript; charset=UTF-8',
|
|
'Content-Length': Buffer.byteLength(s),
|
|
});
|
|
res.write(s);
|
|
res.end();
|
|
} else {
|
|
const status = req.method === 'OPTIONS' ? 200 : 405;
|
|
res.writeHead(status, {
|
|
Allow: 'OPTIONS, GET, HEAD',
|
|
'Content-Length': '0',
|
|
});
|
|
res.end();
|
|
}
|
|
};
|
|
|
|
if (
|
|
_.has(additionalStaticJs, pathname) &&
|
|
!WebAppInternals.inlineScriptsAllowed()
|
|
) {
|
|
serveStaticJs(additionalStaticJs[pathname]);
|
|
return;
|
|
}
|
|
|
|
const { arch, path } = WebApp.categorizeRequest(req);
|
|
|
|
if (!hasOwn.call(WebApp.clientPrograms, arch)) {
|
|
// We could come here in case we run with some architectures excluded
|
|
next();
|
|
return;
|
|
}
|
|
|
|
// If pauseClient(arch) has been called, program.paused will be a
|
|
// Promise that will be resolved when the program is unpaused.
|
|
const program = WebApp.clientPrograms[arch];
|
|
await program.paused;
|
|
|
|
if (
|
|
path === '/meteor_runtime_config.js' &&
|
|
!WebAppInternals.inlineScriptsAllowed()
|
|
) {
|
|
serveStaticJs(
|
|
`__meteor_runtime_config__ = ${program.meteorRuntimeConfig};`
|
|
);
|
|
return;
|
|
}
|
|
|
|
const info = getStaticFileInfo(staticFilesByArch, pathname, path, arch);
|
|
if (!info) {
|
|
next();
|
|
return;
|
|
}
|
|
// "send" will handle HEAD & GET requests
|
|
if (
|
|
req.method !== 'HEAD' &&
|
|
req.method !== 'GET' &&
|
|
!Meteor.settings.packages?.webapp?.alwaysReturnContent
|
|
) {
|
|
const status = req.method === 'OPTIONS' ? 200 : 405;
|
|
res.writeHead(status, {
|
|
Allow: 'OPTIONS, GET, HEAD',
|
|
'Content-Length': '0',
|
|
});
|
|
res.end();
|
|
return;
|
|
}
|
|
|
|
// We don't need to call pause because, unlike 'static', once we call into
|
|
// 'send' and yield to the event loop, we never call another handler with
|
|
// 'next'.
|
|
|
|
// Cacheable files are files that should never change. Typically
|
|
// named by their hash (eg meteor bundled js and css files).
|
|
// We cache them ~forever (1yr).
|
|
const maxAge = info.cacheable ? 1000 * 60 * 60 * 24 * 365 : 0;
|
|
|
|
if (info.cacheable) {
|
|
// Since we use req.headers["user-agent"] to determine whether the
|
|
// client should receive modern or legacy resources, tell the client
|
|
// to invalidate cached resources when/if its user agent string
|
|
// changes in the future.
|
|
res.setHeader('Vary', 'User-Agent');
|
|
}
|
|
|
|
// Set the X-SourceMap header, which current Chrome, FireFox, and Safari
|
|
// understand. (The SourceMap header is slightly more spec-correct but FF
|
|
// doesn't understand it.)
|
|
//
|
|
// You may also need to enable source maps in Chrome: open dev tools, click
|
|
// the gear in the bottom right corner, and select "enable source maps".
|
|
if (info.sourceMapUrl) {
|
|
res.setHeader(
|
|
'X-SourceMap',
|
|
__meteor_runtime_config__.ROOT_URL_PATH_PREFIX + info.sourceMapUrl
|
|
);
|
|
}
|
|
|
|
if (info.type === 'js' || info.type === 'dynamic js') {
|
|
res.setHeader('Content-Type', 'application/javascript; charset=UTF-8');
|
|
} else if (info.type === 'css') {
|
|
res.setHeader('Content-Type', 'text/css; charset=UTF-8');
|
|
} else if (info.type === 'json') {
|
|
res.setHeader('Content-Type', 'application/json; charset=UTF-8');
|
|
}
|
|
|
|
if (info.hash) {
|
|
res.setHeader('ETag', '"' + info.hash + '"');
|
|
}
|
|
|
|
if (info.content) {
|
|
res.setHeader('Content-Length', Buffer.byteLength(info.content));
|
|
res.write(info.content);
|
|
res.end();
|
|
} else {
|
|
send(req, info.absolutePath, {
|
|
maxage: maxAge,
|
|
dotfiles: 'allow', // if we specified a dotfile in the manifest, serve it
|
|
lastModified: false, // don't set last-modified based on the file date
|
|
})
|
|
.on('error', function(err) {
|
|
Log.error('Error serving static file ' + err);
|
|
res.writeHead(500);
|
|
res.end();
|
|
})
|
|
.on('directory', function() {
|
|
Log.error('Unexpected directory ' + info.absolutePath);
|
|
res.writeHead(500);
|
|
res.end();
|
|
})
|
|
.pipe(res);
|
|
}
|
|
};
|
|
|
|
function getStaticFileInfo(staticFilesByArch, originalPath, path, arch) {
|
|
if (!hasOwn.call(WebApp.clientPrograms, arch)) {
|
|
return null;
|
|
}
|
|
|
|
// Get a list of all available static file architectures, with arch
|
|
// first in the list if it exists.
|
|
const staticArchList = Object.keys(staticFilesByArch);
|
|
const archIndex = staticArchList.indexOf(arch);
|
|
if (archIndex > 0) {
|
|
staticArchList.unshift(staticArchList.splice(archIndex, 1)[0]);
|
|
}
|
|
|
|
let info = null;
|
|
|
|
staticArchList.some(arch => {
|
|
const staticFiles = staticFilesByArch[arch];
|
|
|
|
function finalize(path) {
|
|
info = staticFiles[path];
|
|
// Sometimes we register a lazy function instead of actual data in
|
|
// the staticFiles manifest.
|
|
if (typeof info === 'function') {
|
|
info = staticFiles[path] = info();
|
|
}
|
|
return info;
|
|
}
|
|
|
|
// If staticFiles contains originalPath with the arch inferred above,
|
|
// use that information.
|
|
if (hasOwn.call(staticFiles, originalPath)) {
|
|
return finalize(originalPath);
|
|
}
|
|
|
|
// If categorizeRequest returned an alternate path, try that instead.
|
|
if (path !== originalPath && hasOwn.call(staticFiles, path)) {
|
|
return finalize(path);
|
|
}
|
|
});
|
|
|
|
return info;
|
|
}
|
|
|
|
// Parse the passed in port value. Return the port as-is if it's a String
|
|
// (e.g. a Windows Server style named pipe), otherwise return the port as an
|
|
// integer.
|
|
//
|
|
// DEPRECATED: Direct use of this function is not recommended; it is no
|
|
// longer used internally, and will be removed in a future release.
|
|
WebAppInternals.parsePort = port => {
|
|
let parsedPort = parseInt(port);
|
|
if (Number.isNaN(parsedPort)) {
|
|
parsedPort = port;
|
|
}
|
|
return parsedPort;
|
|
};
|
|
|
|
import { onMessage } from 'meteor/inter-process-messaging';
|
|
|
|
onMessage('webapp-pause-client', async ({ arch }) => {
|
|
WebAppInternals.pauseClient(arch);
|
|
});
|
|
|
|
onMessage('webapp-reload-client', async ({ arch }) => {
|
|
WebAppInternals.generateClientProgram(arch);
|
|
});
|
|
|
|
function runWebAppServer() {
|
|
var shuttingDown = false;
|
|
var syncQueue = new Meteor._SynchronousQueue();
|
|
|
|
var getItemPathname = function(itemUrl) {
|
|
return decodeURIComponent(parseUrl(itemUrl).pathname);
|
|
};
|
|
|
|
WebAppInternals.reloadClientPrograms = function() {
|
|
syncQueue.runTask(function() {
|
|
const staticFilesByArch = Object.create(null);
|
|
|
|
const { configJson } = __meteor_bootstrap__;
|
|
const clientArchs =
|
|
configJson.clientArchs || Object.keys(configJson.clientPaths);
|
|
|
|
try {
|
|
clientArchs.forEach(arch => {
|
|
generateClientProgram(arch, staticFilesByArch);
|
|
});
|
|
WebAppInternals.staticFilesByArch = staticFilesByArch;
|
|
} catch (e) {
|
|
Log.error('Error reloading the client program: ' + e.stack);
|
|
process.exit(1);
|
|
}
|
|
});
|
|
};
|
|
|
|
// Pause any incoming requests and make them wait for the program to be
|
|
// unpaused the next time generateClientProgram(arch) is called.
|
|
WebAppInternals.pauseClient = function(arch) {
|
|
syncQueue.runTask(() => {
|
|
const program = WebApp.clientPrograms[arch];
|
|
const { unpause } = program;
|
|
program.paused = new Promise(resolve => {
|
|
if (typeof unpause === 'function') {
|
|
// If there happens to be an existing program.unpause function,
|
|
// compose it with the resolve function.
|
|
program.unpause = function() {
|
|
unpause();
|
|
resolve();
|
|
};
|
|
} else {
|
|
program.unpause = resolve;
|
|
}
|
|
});
|
|
});
|
|
};
|
|
|
|
WebAppInternals.generateClientProgram = function(arch) {
|
|
syncQueue.runTask(() => generateClientProgram(arch));
|
|
};
|
|
|
|
function generateClientProgram(
|
|
arch,
|
|
staticFilesByArch = WebAppInternals.staticFilesByArch
|
|
) {
|
|
const clientDir = pathJoin(
|
|
pathDirname(__meteor_bootstrap__.serverDir),
|
|
arch
|
|
);
|
|
|
|
// read the control for the client we'll be serving up
|
|
const programJsonPath = pathJoin(clientDir, 'program.json');
|
|
|
|
let programJson;
|
|
try {
|
|
programJson = JSON.parse(readFileSync(programJsonPath));
|
|
} catch (e) {
|
|
if (e.code === 'ENOENT') return;
|
|
throw e;
|
|
}
|
|
|
|
if (programJson.format !== 'web-program-pre1') {
|
|
throw new Error(
|
|
'Unsupported format for client assets: ' +
|
|
JSON.stringify(programJson.format)
|
|
);
|
|
}
|
|
|
|
if (!programJsonPath || !clientDir || !programJson) {
|
|
throw new Error('Client config file not parsed.');
|
|
}
|
|
|
|
archPath[arch] = clientDir;
|
|
const staticFiles = (staticFilesByArch[arch] = Object.create(null));
|
|
|
|
const { manifest } = programJson;
|
|
manifest.forEach(item => {
|
|
if (item.url && item.where === 'client') {
|
|
staticFiles[getItemPathname(item.url)] = {
|
|
absolutePath: pathJoin(clientDir, item.path),
|
|
cacheable: item.cacheable,
|
|
hash: item.hash,
|
|
// Link from source to its map
|
|
sourceMapUrl: item.sourceMapUrl,
|
|
type: item.type,
|
|
};
|
|
|
|
if (item.sourceMap) {
|
|
// Serve the source map too, under the specified URL. We assume
|
|
// all source maps are cacheable.
|
|
staticFiles[getItemPathname(item.sourceMapUrl)] = {
|
|
absolutePath: pathJoin(clientDir, item.sourceMap),
|
|
cacheable: true,
|
|
};
|
|
}
|
|
}
|
|
});
|
|
|
|
const { PUBLIC_SETTINGS } = __meteor_runtime_config__;
|
|
const configOverrides = {
|
|
PUBLIC_SETTINGS,
|
|
};
|
|
|
|
const oldProgram = WebApp.clientPrograms[arch];
|
|
const newProgram = (WebApp.clientPrograms[arch] = {
|
|
format: 'web-program-pre1',
|
|
manifest: manifest,
|
|
// Use arrow functions so that these versions can be lazily
|
|
// calculated later, and so that they will not be included in the
|
|
// staticFiles[manifestUrl].content string below.
|
|
//
|
|
// Note: these version calculations must be kept in agreement with
|
|
// CordovaBuilder#appendVersion in tools/cordova/builder.js, or hot
|
|
// code push will reload Cordova apps unnecessarily.
|
|
version: () =>
|
|
WebAppHashing.calculateClientHash(manifest, null, configOverrides),
|
|
versionRefreshable: () =>
|
|
WebAppHashing.calculateClientHash(
|
|
manifest,
|
|
type => type === 'css',
|
|
configOverrides
|
|
),
|
|
versionNonRefreshable: () =>
|
|
WebAppHashing.calculateClientHash(
|
|
manifest,
|
|
(type, replaceable) => type !== 'css' && !replaceable,
|
|
configOverrides
|
|
),
|
|
versionReplaceable: () =>
|
|
WebAppHashing.calculateClientHash(
|
|
manifest,
|
|
(_type, replaceable) => replaceable,
|
|
configOverrides
|
|
),
|
|
cordovaCompatibilityVersions: programJson.cordovaCompatibilityVersions,
|
|
PUBLIC_SETTINGS,
|
|
hmrVersion: programJson.hmrVersion,
|
|
});
|
|
|
|
// Expose program details as a string reachable via the following URL.
|
|
const manifestUrlPrefix = '/__' + arch.replace(/^web\./, '');
|
|
const manifestUrl = manifestUrlPrefix + getItemPathname('/manifest.json');
|
|
|
|
staticFiles[manifestUrl] = () => {
|
|
if (Package.autoupdate) {
|
|
const {
|
|
AUTOUPDATE_VERSION = Package.autoupdate.Autoupdate.autoupdateVersion,
|
|
} = process.env;
|
|
|
|
if (AUTOUPDATE_VERSION) {
|
|
newProgram.version = AUTOUPDATE_VERSION;
|
|
}
|
|
}
|
|
|
|
if (typeof newProgram.version === 'function') {
|
|
newProgram.version = newProgram.version();
|
|
}
|
|
|
|
return {
|
|
content: JSON.stringify(newProgram),
|
|
cacheable: false,
|
|
hash: newProgram.version,
|
|
type: 'json',
|
|
};
|
|
};
|
|
|
|
generateBoilerplateForArch(arch);
|
|
|
|
// If there are any requests waiting on oldProgram.paused, let them
|
|
// continue now (using the new program).
|
|
if (oldProgram && oldProgram.paused) {
|
|
oldProgram.unpause();
|
|
}
|
|
}
|
|
|
|
const defaultOptionsForArch = {
|
|
'web.cordova': {
|
|
runtimeConfigOverrides: {
|
|
// XXX We use absoluteUrl() here so that we serve https://
|
|
// URLs to cordova clients if force-ssl is in use. If we were
|
|
// to use __meteor_runtime_config__.ROOT_URL instead of
|
|
// absoluteUrl(), then Cordova clients would immediately get a
|
|
// HCP setting their DDP_DEFAULT_CONNECTION_URL to
|
|
// http://example.meteor.com. This breaks the app, because
|
|
// force-ssl doesn't serve CORS headers on 302
|
|
// redirects. (Plus it's undesirable to have clients
|
|
// connecting to http://example.meteor.com when force-ssl is
|
|
// in use.)
|
|
DDP_DEFAULT_CONNECTION_URL:
|
|
process.env.MOBILE_DDP_URL || Meteor.absoluteUrl(),
|
|
ROOT_URL: process.env.MOBILE_ROOT_URL || Meteor.absoluteUrl(),
|
|
},
|
|
},
|
|
|
|
'web.browser': {
|
|
runtimeConfigOverrides: {
|
|
isModern: true,
|
|
},
|
|
},
|
|
|
|
'web.browser.legacy': {
|
|
runtimeConfigOverrides: {
|
|
isModern: false,
|
|
},
|
|
},
|
|
};
|
|
|
|
WebAppInternals.generateBoilerplate = function() {
|
|
// This boilerplate will be served to the mobile devices when used with
|
|
// Meteor/Cordova for the Hot-Code Push and since the file will be served by
|
|
// the device's server, it is important to set the DDP url to the actual
|
|
// Meteor server accepting DDP connections and not the device's file server.
|
|
syncQueue.runTask(function() {
|
|
Object.keys(WebApp.clientPrograms).forEach(generateBoilerplateForArch);
|
|
});
|
|
};
|
|
|
|
function generateBoilerplateForArch(arch) {
|
|
const program = WebApp.clientPrograms[arch];
|
|
const additionalOptions = defaultOptionsForArch[arch] || {};
|
|
const { baseData } = (boilerplateByArch[
|
|
arch
|
|
] = WebAppInternals.generateBoilerplateInstance(
|
|
arch,
|
|
program.manifest,
|
|
additionalOptions
|
|
));
|
|
// We need the runtime config with overrides for meteor_runtime_config.js:
|
|
program.meteorRuntimeConfig = JSON.stringify({
|
|
...__meteor_runtime_config__,
|
|
...(additionalOptions.runtimeConfigOverrides || null),
|
|
});
|
|
program.refreshableAssets = baseData.css.map(file => ({
|
|
url: bundledJsCssUrlRewriteHook(file.url),
|
|
}));
|
|
}
|
|
|
|
WebAppInternals.reloadClientPrograms();
|
|
|
|
// webserver
|
|
var app = connect();
|
|
|
|
// Packages and apps can add handlers that run before any other Meteor
|
|
// handlers via WebApp.rawConnectHandlers.
|
|
var rawConnectHandlers = connect();
|
|
app.use(rawConnectHandlers);
|
|
|
|
// Auto-compress any json, javascript, or text.
|
|
app.use(compress({ filter: shouldCompress }));
|
|
|
|
// parse cookies into an object
|
|
app.use(cookieParser());
|
|
|
|
// We're not a proxy; reject (without crashing) attempts to treat us like
|
|
// one. (See #1212.)
|
|
app.use(function(req, res, next) {
|
|
if (RoutePolicy.isValidUrl(req.url)) {
|
|
next();
|
|
return;
|
|
}
|
|
res.writeHead(400);
|
|
res.write('Not a proxy');
|
|
res.end();
|
|
});
|
|
|
|
// Parse the query string into res.query. Used by oauth_server, but it's
|
|
// generally pretty handy..
|
|
//
|
|
// Do this before the next middleware destroys req.url if a path prefix
|
|
// is set to close #10111.
|
|
app.use(function(request, response, next) {
|
|
request.query = qs.parse(parseUrl(request.url).query);
|
|
next();
|
|
});
|
|
|
|
function getPathParts(path) {
|
|
const parts = path.split('/');
|
|
while (parts[0] === '') parts.shift();
|
|
return parts;
|
|
}
|
|
|
|
function isPrefixOf(prefix, array) {
|
|
return (
|
|
prefix.length <= array.length &&
|
|
prefix.every((part, i) => part === array[i])
|
|
);
|
|
}
|
|
|
|
// Strip off the path prefix, if it exists.
|
|
app.use(function(request, response, next) {
|
|
const pathPrefix = __meteor_runtime_config__.ROOT_URL_PATH_PREFIX;
|
|
const { pathname, search } = parseUrl(request.url);
|
|
|
|
// check if the path in the url starts with the path prefix
|
|
if (pathPrefix) {
|
|
const prefixParts = getPathParts(pathPrefix);
|
|
const pathParts = getPathParts(pathname);
|
|
if (isPrefixOf(prefixParts, pathParts)) {
|
|
request.url = '/' + pathParts.slice(prefixParts.length).join('/');
|
|
if (search) {
|
|
request.url += search;
|
|
}
|
|
return next();
|
|
}
|
|
}
|
|
|
|
if (pathname === '/favicon.ico' || pathname === '/robots.txt') {
|
|
return next();
|
|
}
|
|
|
|
if (pathPrefix) {
|
|
response.writeHead(404);
|
|
response.write('Unknown path');
|
|
response.end();
|
|
return;
|
|
}
|
|
|
|
next();
|
|
});
|
|
|
|
// Serve static files from the manifest.
|
|
// This is inspired by the 'static' middleware.
|
|
app.use(function(req, res, next) {
|
|
WebAppInternals.staticFilesMiddleware(
|
|
WebAppInternals.staticFilesByArch,
|
|
req,
|
|
res,
|
|
next
|
|
);
|
|
});
|
|
|
|
// Core Meteor packages like dynamic-import can add handlers before
|
|
// other handlers added by package and application code.
|
|
app.use((WebAppInternals.meteorInternalHandlers = connect()));
|
|
|
|
/**
|
|
* @name connectHandlersCallback(req, res, next)
|
|
* @locus Server
|
|
* @isprototype true
|
|
* @summary callback handler for `WebApp.connectHandlers`
|
|
* @param {Object} req
|
|
* a Node.js
|
|
* [IncomingMessage](https://nodejs.org/api/http.html#http_class_http_incomingmessage)
|
|
* object with some extra properties. This argument can be used
|
|
* to get information about the incoming request.
|
|
* @param {Object} res
|
|
* a Node.js
|
|
* [ServerResponse](http://nodejs.org/api/http.html#http_class_http_serverresponse)
|
|
* object. Use this to write data that should be sent in response to the
|
|
* request, and call `res.end()` when you are done.
|
|
* @param {Function} next
|
|
* Calling this function will pass on the handling of
|
|
* this request to the next relevant handler.
|
|
*
|
|
*/
|
|
|
|
/**
|
|
* @method connectHandlers
|
|
* @memberof WebApp
|
|
* @locus Server
|
|
* @summary Register a handler for all HTTP requests.
|
|
* @param {String} [path]
|
|
* This handler will only be called on paths that match
|
|
* this string. The match has to border on a `/` or a `.`.
|
|
*
|
|
* For example, `/hello` will match `/hello/world` and
|
|
* `/hello.world`, but not `/hello_world`.
|
|
* @param {connectHandlersCallback} handler
|
|
* A handler function that will be called on HTTP requests.
|
|
* See `connectHandlersCallback`
|
|
*
|
|
*/
|
|
// Packages and apps can add handlers to this via WebApp.connectHandlers.
|
|
// They are inserted before our default handler.
|
|
var packageAndAppHandlers = connect();
|
|
app.use(packageAndAppHandlers);
|
|
|
|
var suppressConnectErrors = false;
|
|
// connect knows it is an error handler because it has 4 arguments instead of
|
|
// 3. go figure. (It is not smart enough to find such a thing if it's hidden
|
|
// inside packageAndAppHandlers.)
|
|
app.use(function(err, req, res, next) {
|
|
if (!err || !suppressConnectErrors || !req.headers['x-suppress-error']) {
|
|
next(err);
|
|
return;
|
|
}
|
|
res.writeHead(err.status, { 'Content-Type': 'text/plain' });
|
|
res.end('An error message');
|
|
});
|
|
|
|
app.use(async function(req, res, next) {
|
|
if (!appUrl(req.url)) {
|
|
return next();
|
|
} else if (
|
|
req.method !== 'HEAD' &&
|
|
req.method !== 'GET' &&
|
|
!Meteor.settings.packages?.webapp?.alwaysReturnContent
|
|
) {
|
|
const status = req.method === 'OPTIONS' ? 200 : 405;
|
|
res.writeHead(status, {
|
|
Allow: 'OPTIONS, GET, HEAD',
|
|
'Content-Length': '0',
|
|
});
|
|
res.end();
|
|
} else {
|
|
var headers = {
|
|
'Content-Type': 'text/html; charset=utf-8',
|
|
};
|
|
|
|
if (shuttingDown) {
|
|
headers['Connection'] = 'Close';
|
|
}
|
|
|
|
var request = WebApp.categorizeRequest(req);
|
|
|
|
if (request.url.query && request.url.query['meteor_css_resource']) {
|
|
// In this case, we're requesting a CSS resource in the meteor-specific
|
|
// way, but we don't have it. Serve a static css file that indicates that
|
|
// we didn't have it, so we can detect that and refresh. Make sure
|
|
// that any proxies or CDNs don't cache this error! (Normally proxies
|
|
// or CDNs are smart enough not to cache error pages, but in order to
|
|
// make this hack work, we need to return the CSS file as a 200, which
|
|
// would otherwise be cached.)
|
|
headers['Content-Type'] = 'text/css; charset=utf-8';
|
|
headers['Cache-Control'] = 'no-cache';
|
|
res.writeHead(200, headers);
|
|
res.write('.meteor-css-not-found-error { width: 0px;}');
|
|
res.end();
|
|
return;
|
|
}
|
|
|
|
if (request.url.query && request.url.query['meteor_js_resource']) {
|
|
// Similarly, we're requesting a JS resource that we don't have.
|
|
// Serve an uncached 404. (We can't use the same hack we use for CSS,
|
|
// because actually acting on that hack requires us to have the JS
|
|
// already!)
|
|
headers['Cache-Control'] = 'no-cache';
|
|
res.writeHead(404, headers);
|
|
res.end('404 Not Found');
|
|
return;
|
|
}
|
|
|
|
if (request.url.query && request.url.query['meteor_dont_serve_index']) {
|
|
// When downloading files during a Cordova hot code push, we need
|
|
// to detect if a file is not available instead of inadvertently
|
|
// downloading the default index page.
|
|
// So similar to the situation above, we serve an uncached 404.
|
|
headers['Cache-Control'] = 'no-cache';
|
|
res.writeHead(404, headers);
|
|
res.end('404 Not Found');
|
|
return;
|
|
}
|
|
|
|
const { arch } = request;
|
|
assert.strictEqual(typeof arch, 'string', { arch });
|
|
|
|
if (!hasOwn.call(WebApp.clientPrograms, arch)) {
|
|
// We could come here in case we run with some architectures excluded
|
|
headers['Cache-Control'] = 'no-cache';
|
|
res.writeHead(404, headers);
|
|
if (Meteor.isDevelopment) {
|
|
res.end(`No client program found for the ${arch} architecture.`);
|
|
} else {
|
|
// Safety net, but this branch should not be possible.
|
|
res.end('404 Not Found');
|
|
}
|
|
return;
|
|
}
|
|
|
|
// If pauseClient(arch) has been called, program.paused will be a
|
|
// Promise that will be resolved when the program is unpaused.
|
|
await WebApp.clientPrograms[arch].paused;
|
|
|
|
return getBoilerplateAsync(request, arch)
|
|
.then(({ stream, statusCode, headers: newHeaders }) => {
|
|
if (!statusCode) {
|
|
statusCode = res.statusCode ? res.statusCode : 200;
|
|
}
|
|
|
|
if (newHeaders) {
|
|
Object.assign(headers, newHeaders);
|
|
}
|
|
|
|
res.writeHead(statusCode, headers);
|
|
|
|
stream.pipe(res, {
|
|
// End the response when the stream ends.
|
|
end: true,
|
|
});
|
|
})
|
|
.catch(error => {
|
|
Log.error('Error running template: ' + error.stack);
|
|
res.writeHead(500, headers);
|
|
res.end();
|
|
});
|
|
}
|
|
});
|
|
|
|
// Return 404 by default, if no other handlers serve this URL.
|
|
app.use(function(req, res) {
|
|
res.writeHead(404);
|
|
res.end();
|
|
});
|
|
|
|
var httpServer = createServer(app);
|
|
var onListeningCallbacks = [];
|
|
|
|
// After 5 seconds w/o data on a socket, kill it. On the other hand, if
|
|
// there's an outstanding request, give it a higher timeout instead (to avoid
|
|
// killing long-polling requests)
|
|
httpServer.setTimeout(SHORT_SOCKET_TIMEOUT);
|
|
|
|
// Do this here, and then also in livedata/stream_server.js, because
|
|
// stream_server.js kills all the current request handlers when installing its
|
|
// own.
|
|
httpServer.on('request', WebApp._timeoutAdjustmentRequestCallback);
|
|
|
|
// If the client gave us a bad request, tell it instead of just closing the
|
|
// socket. This lets load balancers in front of us differentiate between "a
|
|
// server is randomly closing sockets for no reason" and "client sent a bad
|
|
// request".
|
|
//
|
|
// This will only work on Node 6; Node 4 destroys the socket before calling
|
|
// this event. See https://github.com/nodejs/node/pull/4557/ for details.
|
|
httpServer.on('clientError', (err, socket) => {
|
|
// Pre-Node-6, do nothing.
|
|
if (socket.destroyed) {
|
|
return;
|
|
}
|
|
|
|
if (err.message === 'Parse Error') {
|
|
socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
|
|
} else {
|
|
// For other errors, use the default behavior as if we had no clientError
|
|
// handler.
|
|
socket.destroy(err);
|
|
}
|
|
});
|
|
|
|
// start up app
|
|
_.extend(WebApp, {
|
|
connectHandlers: packageAndAppHandlers,
|
|
rawConnectHandlers: rawConnectHandlers,
|
|
httpServer: httpServer,
|
|
connectApp: app,
|
|
// For testing.
|
|
suppressConnectErrors: function() {
|
|
suppressConnectErrors = true;
|
|
},
|
|
onListening: function(f) {
|
|
if (onListeningCallbacks) onListeningCallbacks.push(f);
|
|
else f();
|
|
},
|
|
// This can be overridden by users who want to modify how listening works
|
|
// (eg, to run a proxy like Apollo Engine Proxy in front of the server).
|
|
startListening: function(httpServer, listenOptions, cb) {
|
|
httpServer.listen(listenOptions, cb);
|
|
},
|
|
});
|
|
|
|
/**
|
|
* @name main
|
|
* @locus Server
|
|
* @summary Starts the HTTP server.
|
|
* If `UNIX_SOCKET_PATH` is present Meteor's HTTP server will use that socket file for inter-process communication, instead of TCP.
|
|
* If you choose to not include webapp package in your application this method still must be defined for your Meteor application to work.
|
|
*/
|
|
// Let the rest of the packages (and Meteor.startup hooks) insert connect
|
|
// middlewares and update __meteor_runtime_config__, then keep going to set up
|
|
// actually serving HTML.
|
|
exports.main = argv => {
|
|
WebAppInternals.generateBoilerplate();
|
|
|
|
const startHttpServer = listenOptions => {
|
|
WebApp.startListening(
|
|
httpServer,
|
|
listenOptions,
|
|
Meteor.bindEnvironment(
|
|
() => {
|
|
if (process.env.METEOR_PRINT_ON_LISTEN) {
|
|
console.log('LISTENING');
|
|
}
|
|
const callbacks = onListeningCallbacks;
|
|
onListeningCallbacks = null;
|
|
callbacks.forEach(callback => {
|
|
callback();
|
|
});
|
|
},
|
|
e => {
|
|
console.error('Error listening:', e);
|
|
console.error(e && e.stack);
|
|
}
|
|
)
|
|
);
|
|
};
|
|
|
|
let localPort = process.env.PORT || 0;
|
|
let unixSocketPath = process.env.UNIX_SOCKET_PATH;
|
|
|
|
if (unixSocketPath) {
|
|
if (cluster.isWorker) {
|
|
const workerName = cluster.worker.process.env.name || cluster.worker.id;
|
|
unixSocketPath += '.' + workerName + '.sock';
|
|
}
|
|
// Start the HTTP server using a socket file.
|
|
removeExistingSocketFile(unixSocketPath);
|
|
startHttpServer({ path: unixSocketPath });
|
|
|
|
const unixSocketPermissions = (
|
|
process.env.UNIX_SOCKET_PERMISSIONS || ''
|
|
).trim();
|
|
if (unixSocketPermissions) {
|
|
if (/^[0-7]{3}$/.test(unixSocketPermissions)) {
|
|
chmodSync(unixSocketPath, parseInt(unixSocketPermissions, 8));
|
|
} else {
|
|
throw new Error('Invalid UNIX_SOCKET_PERMISSIONS specified');
|
|
}
|
|
}
|
|
|
|
const unixSocketGroup = (process.env.UNIX_SOCKET_GROUP || '').trim();
|
|
if (unixSocketGroup) {
|
|
//whomst automatically handles both group names and numerical gids
|
|
const unixSocketGroupInfo = whomst.sync.group(unixSocketGroup);
|
|
if (unixSocketGroupInfo === null) {
|
|
throw new Error('Invalid UNIX_SOCKET_GROUP name specified');
|
|
}
|
|
chownSync(unixSocketPath, userInfo().uid, unixSocketGroupInfo.gid);
|
|
}
|
|
|
|
registerSocketFileCleanup(unixSocketPath);
|
|
} else {
|
|
localPort = isNaN(Number(localPort)) ? localPort : Number(localPort);
|
|
if (/\\\\?.+\\pipe\\?.+/.test(localPort)) {
|
|
// Start the HTTP server using Windows Server style named pipe.
|
|
startHttpServer({ path: localPort });
|
|
} else if (typeof localPort === 'number') {
|
|
// Start the HTTP server using TCP.
|
|
startHttpServer({
|
|
port: localPort,
|
|
host: process.env.BIND_IP || '0.0.0.0',
|
|
});
|
|
} else {
|
|
throw new Error('Invalid PORT specified');
|
|
}
|
|
}
|
|
|
|
return 'DAEMON';
|
|
};
|
|
}
|
|
|
|
var inlineScriptsAllowed = true;
|
|
|
|
WebAppInternals.inlineScriptsAllowed = function() {
|
|
return inlineScriptsAllowed;
|
|
};
|
|
|
|
WebAppInternals.setInlineScriptsAllowed = function(value) {
|
|
inlineScriptsAllowed = value;
|
|
WebAppInternals.generateBoilerplate();
|
|
};
|
|
|
|
var sriMode;
|
|
|
|
WebAppInternals.enableSubresourceIntegrity = function(use_credentials = false) {
|
|
sriMode = use_credentials ? 'use-credentials' : 'anonymous';
|
|
WebAppInternals.generateBoilerplate();
|
|
};
|
|
|
|
WebAppInternals.setBundledJsCssUrlRewriteHook = function(hookFn) {
|
|
bundledJsCssUrlRewriteHook = hookFn;
|
|
WebAppInternals.generateBoilerplate();
|
|
};
|
|
|
|
WebAppInternals.setBundledJsCssPrefix = function(prefix) {
|
|
var self = this;
|
|
self.setBundledJsCssUrlRewriteHook(function(url) {
|
|
return prefix + url;
|
|
});
|
|
};
|
|
|
|
// Packages can call `WebAppInternals.addStaticJs` to specify static
|
|
// JavaScript to be included in the app. This static JS will be inlined,
|
|
// unless inline scripts have been disabled, in which case it will be
|
|
// served under `/<sha1 of contents>`.
|
|
var additionalStaticJs = {};
|
|
WebAppInternals.addStaticJs = function(contents) {
|
|
additionalStaticJs['/' + sha1(contents) + '.js'] = contents;
|
|
};
|
|
|
|
// Exported for tests
|
|
WebAppInternals.getBoilerplate = getBoilerplate;
|
|
WebAppInternals.additionalStaticJs = additionalStaticJs;
|
|
|
|
// Start the server!
|
|
runWebAppServer();
|