Files
meteor/packages/webapp/webapp_server.js
Ben Newman fb73388ce3 Make server-render API more flexible and isomorph-ish.
Render callbacks can now inject HTML content into multiple different
elements, and may also append content to the <head> or <body> elements, on
both the client and the server.

This new API was inspired by trying to use the styled-components npm
package on the server, which involves not only rendering and injecting
static HTML somewhere in the <body>, but also appending the resulting
<style> tag(s) into the <head>:

  import { onPageLoad } from "meteor/server-render";
  import { renderToString } from "react-dom/server";
  import { ServerStyleSheet } from "styled-components";

  onPageLoad(sink => {
    const sheet = new ServerStyleSheet();
    const html = renderToString(sheet.collectStyles(
      <App location={sink.request.url} />
    ));

    sink.renderIntoElementById("app", html);
    sink.appendToHead(sheet.getStyleTags());
  });

Note that the server-render package now exports an onPageLoad function,
rather than the old renderIntoElementById function. The functionality of
renderIntoElementById is now exposed by the {Client,Server}Sink API.

I say the client-side version of this API is 'isomorphish' to the
server-side version, because ClientSink methods can accept DOM nodes in
addition to raw HTML strings, whereas DOM nodes don't really make sense on
the server.
2017-06-29 15:08:32 -04:00

925 lines
31 KiB
JavaScript

import assert from "assert";
import { readFile } from "fs";
import { createServer } from "http";
import {
join as pathJoin,
dirname as pathDirname,
} from "path";
import { parse as parseUrl } from "url";
import { createHash } from "crypto";
import connect from "connect";
import parseRequest from "parseurl";
import { lookup as lookupUserAgent } from "useragent";
import send from "send";
var SHORT_SOCKET_TIMEOUT = 5*1000;
var LONG_SOCKET_TIMEOUT = 120*1000;
export const WebApp = {};
export const WebAppInternals = {};
WebAppInternals.NpmModules = {
connect: {
version: Npm.require('connect/package.json').version,
module: connect
}
};
WebApp.defaultArch = 'web.browser';
// 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');
};
var readUtf8FileSync = function (filename) {
return Meteor.wrapAsync(readFile)(filename, 'utf8');
};
// #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) {
return _.extend({
browser: identifyBrowser(req.headers['user-agent']),
url: parseUrl(req.url, true)
}, _.pick(req, 'dynamicHead', 'dynamicBody'));
};
// 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 () {
var calculateClientHash = WebAppHashing.calculateClientHash;
WebApp.clientHash = function (archName) {
archName = archName || WebApp.defaultArch;
return calculateClientHash(WebApp.clientPrograms[archName].manifest);
};
WebApp.calculateClientHashRefreshable = function (archName) {
archName = archName || WebApp.defaultArch;
return calculateClientHash(WebApp.clientPrograms[archName].manifest,
function (name) {
return name === "css";
});
};
WebApp.calculateClientHashNonRefreshable = function (archName) {
archName = archName || WebApp.defaultArch;
return calculateClientHash(WebApp.clientPrograms[archName].manifest,
function (name) {
return name !== "css";
});
};
WebApp.calculateClientHashCordova = function () {
var archName = 'web.cordova';
if (! WebApp.clientPrograms[archName])
return 'none';
return calculateClientHash(
WebApp.clientPrograms[archName].manifest, null, _.pick(
__meteor_runtime_config__, 'PUBLIC_SETTINGS'));
};
});
// 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'
var memoizedBoilerplate = {};
function getBoilerplate(request, arch) {
return getBoilerplateAsync(request, arch).await();
}
function getBoilerplateAsync(request, arch) {
const boilerplate = boilerplateByArch[arch];
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(() => {
const useMemoized = ! (
data.dynamicHead ||
data.dynamicBody ||
madeChanges
);
if (! useMemoized) {
return boilerplate.toHTML(data);
}
// The only thing that changes from request to request (unless extra
// content is added to the head or body, or boilerplateDataCallbacks
// modified the data) are the HTML attributes (used by, eg, appcache)
// and whether inline scripts are allowed, so memoize based on that.
var memHash = JSON.stringify({
inlineScriptsAllowed,
htmlAttributes: data.htmlAttributes,
arch,
});
if (! memoizedBoilerplate[memHash]) {
memoizedBoilerplate[memHash] =
boilerplateByArch[arch].toHTML(data);
}
return memoizedBoilerplate[memHash];
});
}
WebAppInternals.generateBoilerplateInstance = function (arch,
manifest,
additionalOptions) {
additionalOptions = additionalOptions || {};
var runtimeConfig = _.extend(
_.clone(__meteor_runtime_config__),
additionalOptions.runtimeConfigOverrides || {}
);
return new Boilerplate(arch, manifest,
_.extend({
pathMapper: function (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: JSON.stringify(
encodeURIComponent(JSON.stringify(runtimeConfig))),
rootUrlPathPrefix: __meteor_runtime_config__.ROOT_URL_PATH_PREFIX || '',
bundledJsCssUrlRewriteHook: bundledJsCssUrlRewriteHook,
inlineScriptsAllowed: WebAppInternals.inlineScriptsAllowed(),
inline: additionalOptions.inline
}
}, additionalOptions)
);
};
// A mapping from url path to "info". Where "info" has 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
var staticFiles;
// Serve static files from the manifest or added with
// `addStaticJs`. Exported for tests.
WebAppInternals.staticFilesMiddleware = function (staticFiles, req, res, next) {
if ('GET' != req.method && 'HEAD' != req.method && 'OPTIONS' != req.method) {
next();
return;
}
var pathname = parseRequest(req).pathname;
try {
pathname = decodeURIComponent(pathname);
} catch (e) {
next();
return;
}
var serveStaticJs = function (s) {
res.writeHead(200, {
'Content-type': 'application/javascript; charset=UTF-8'
});
res.write(s);
res.end();
};
if (pathname === "/meteor_runtime_config.js" &&
! WebAppInternals.inlineScriptsAllowed()) {
serveStaticJs("__meteor_runtime_config__ = " +
JSON.stringify(__meteor_runtime_config__) + ";");
return;
} else if (_.has(additionalStaticJs, pathname) &&
! WebAppInternals.inlineScriptsAllowed()) {
serveStaticJs(additionalStaticJs[pathname]);
return;
}
if (!_.has(staticFiles, pathname)) {
next();
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'.
var info = staticFiles[pathname];
// 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).
var maxAge = info.cacheable
? 1000 * 60 * 60 * 24 * 365
: 0;
// 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.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);
}
};
var getUrlPrefixForArch = function (arch) {
// XXX we rely on the fact that arch names don't contain slashes
// in that case we would need to uri escape it
// We add '__' to the beginning of non-standard archs to "scope" the url
// to Meteor internals.
return arch === WebApp.defaultArch ?
'' : '/' + '__' + arch.replace(/^web\./, '');
};
// parse port to see if its a Windows Server style named pipe. If so, return as-is (String), otherwise return as Int
WebAppInternals.parsePort = function (port) {
if( /\\\\?.+\\pipe\\?.+/.test(port) ) {
return port;
}
return parseInt(port);
};
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() {
staticFiles = {};
var generateClientProgram = function (clientPath, arch) {
// read the control for the client we'll be serving up
var clientJsonPath = pathJoin(__meteor_bootstrap__.serverDir,
clientPath);
var clientDir = pathDirname(clientJsonPath);
var clientJson = JSON.parse(readUtf8FileSync(clientJsonPath));
if (clientJson.format !== "web-program-pre1")
throw new Error("Unsupported format for client assets: " +
JSON.stringify(clientJson.format));
if (! clientJsonPath || ! clientDir || ! clientJson)
throw new Error("Client config file not parsed.");
var urlPrefix = getUrlPrefixForArch(arch);
var manifest = clientJson.manifest;
_.each(manifest, function (item) {
if (item.url && item.where === "client") {
staticFiles[urlPrefix + 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[urlPrefix + getItemPathname(item.sourceMapUrl)] = {
absolutePath: pathJoin(clientDir, item.sourceMap),
cacheable: true
};
}
}
});
var program = {
format: "web-program-pre1",
manifest: manifest,
version: process.env.AUTOUPDATE_VERSION ||
WebAppHashing.calculateClientHash(
manifest,
null,
_.pick(__meteor_runtime_config__, "PUBLIC_SETTINGS")
),
cordovaCompatibilityVersions: clientJson.cordovaCompatibilityVersions,
PUBLIC_SETTINGS: __meteor_runtime_config__.PUBLIC_SETTINGS
};
WebApp.clientPrograms[arch] = program;
// Serve the program as a string at /foo/<arch>/manifest.json
// XXX change manifest.json -> program.json
staticFiles[urlPrefix + getItemPathname('/manifest.json')] = {
content: JSON.stringify(program),
cacheable: false,
hash: program.version,
type: "json"
};
};
try {
var clientPaths = __meteor_bootstrap__.configJson.clientPaths;
_.each(clientPaths, function (clientPath, arch) {
archPath[arch] = pathDirname(clientPath);
generateClientProgram(clientPath, arch);
});
// Exported for tests.
WebAppInternals.staticFiles = staticFiles;
} catch (e) {
Log.error("Error reloading the client program: " + e.stack);
process.exit(1);
}
});
};
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.
var 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()
}
}
};
syncQueue.runTask(function() {
_.each(WebApp.clientPrograms, function (program, archName) {
boilerplateByArch[archName] =
WebAppInternals.generateBoilerplateInstance(
archName, program.manifest,
defaultOptionsForArch[archName]);
});
// Clear the memoized boilerplate cache.
memoizedBoilerplate = {};
// Configure CSS injection for the default arch
// XXX implement the CSS injection for all archs?
var cssFiles = boilerplateByArch[WebApp.defaultArch].baseData.css;
// Rewrite all CSS files (which are written directly to <style> tags)
// by autoupdate_client to use the CDN prefix/etc
var allCss = _.map(cssFiles, function(cssFile) {
return { url: bundledJsCssUrlRewriteHook(cssFile.url) };
});
WebAppInternals.refreshableAssets = { allCss };
});
};
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(connect.compress());
// 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();
});
// Strip off the path prefix, if it exists.
app.use(function (request, response, next) {
var pathPrefix = __meteor_runtime_config__.ROOT_URL_PATH_PREFIX;
var url = Npm.require('url').parse(request.url);
var pathname = url.pathname;
// check if the path in the url starts with the path prefix (and the part
// after the path prefix must start with a / if it exists.)
if (pathPrefix && pathname.substring(0, pathPrefix.length) === pathPrefix &&
(pathname.length == pathPrefix.length
|| pathname.substring(pathPrefix.length, pathPrefix.length + 1) === "/")) {
request.url = request.url.substring(pathPrefix.length);
next();
} else if (pathname === "/favicon.ico" || pathname === "/robots.txt") {
next();
} else if (pathPrefix) {
response.writeHead(404);
response.write("Unknown path");
response.end();
} else {
next();
}
});
// Parse the query string into res.query. Used by oauth_server, but it's
// generally pretty handy..
app.use(connect.query());
// Serve static files from the manifest.
// This is inspired by the 'static' middleware.
app.use(function (req, res, next) {
Promise.resolve().then(() => {
WebAppInternals.staticFilesMiddleware(staticFiles, req, res, next);
});
});
// 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(function (req, res, next) {
Promise.resolve().then(() => {
if (! appUrl(req.url)) {
return next();
}
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;
}
// /packages/asdfsad ... /__cordova/dafsdf.js
var pathname = parseRequest(req).pathname;
var archKey = pathname.split('/')[1];
var archKeyCleaned = 'web.' + archKey.replace(/^__/, '');
if (!/^__/.test(archKey) || !_.has(archPath, archKeyCleaned)) {
archKey = WebApp.defaultArch;
} else {
archKey = archKeyCleaned;
}
return getBoilerplateAsync(
request,
archKey
).then(boilerplate => {
var statusCode = res.statusCode ? res.statusCode : 200;
res.writeHead(statusCode, headers);
res.write(boilerplate);
res.end();
}, 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();
}
});
// 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 = function (argv) {
WebAppInternals.generateBoilerplate();
// only start listening after all the startup code has run.
var localPort = WebAppInternals.parsePort(process.env.PORT) || 0;
var host = process.env.BIND_IP;
var localIp = host || '0.0.0.0';
httpServer.listen(localPort, localIp, Meteor.bindEnvironment(function() {
if (process.env.METEOR_PRINT_ON_LISTEN) {
console.log("LISTENING"); // must match run-app.js
}
var callbacks = onListeningCallbacks;
onListeningCallbacks = null;
_.each(callbacks, function (x) { x(); });
}, function (e) {
console.error("Error listening:", e);
console.error(e && e.stack);
}));
return 'DAEMON';
};
}
runWebAppServer();
var inlineScriptsAllowed = true;
WebAppInternals.inlineScriptsAllowed = function () {
return inlineScriptsAllowed;
};
WebAppInternals.setInlineScriptsAllowed = function (value) {
inlineScriptsAllowed = value;
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;