mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
Previously we used the Node default behavior of just closing the socket. This made it difficult for load balancers to understand if the backend is buggy or the client request was bad (if Node is validating something more strictly than the load balancer). This only takes effect on Node 6; in older versions the socket is already destroyed before the event is invoked. See https://github.com/nodejs/node/pull/4557/ for details.
871 lines
30 KiB
JavaScript
871 lines
30 KiB
JavaScript
////////// Requires //////////
|
|
|
|
var fs = Npm.require("fs");
|
|
var http = Npm.require("http");
|
|
var os = Npm.require("os");
|
|
var path = Npm.require("path");
|
|
var url = Npm.require("url");
|
|
var crypto = Npm.require("crypto");
|
|
|
|
var connect = Npm.require('connect');
|
|
var parseurl = Npm.require('parseurl');
|
|
var useragent = Npm.require('useragent');
|
|
var send = Npm.require('send');
|
|
|
|
var Future = Npm.require('fibers/future');
|
|
var Fiber = Npm.require('fibers');
|
|
|
|
var SHORT_SOCKET_TIMEOUT = 5*1000;
|
|
var LONG_SOCKET_TIMEOUT = 120*1000;
|
|
|
|
WebApp = {};
|
|
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 = crypto.createHash('sha1');
|
|
hash.update(contents);
|
|
return hash.digest('hex');
|
|
};
|
|
|
|
var readUtf8FileSync = function (filename) {
|
|
return Meteor.wrapAsync(fs.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 = useragent.lookup(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: url.parse(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 = {};
|
|
|
|
// 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 = {};
|
|
var getBoilerplate = function (request, arch) {
|
|
var useMemoized = ! (request.dynamicHead || request.dynamicBody);
|
|
var htmlAttributes = getHtmlAttributes(request);
|
|
|
|
if (useMemoized) {
|
|
// The only thing that changes from request to request (unless extra
|
|
// content is added to the head or body) are the HTML attributes
|
|
// (used by, eg, appcache) and whether inline scripts are allowed, so we
|
|
// can memoize based on that.
|
|
var memHash = JSON.stringify({
|
|
inlineScriptsAllowed: inlineScriptsAllowed,
|
|
htmlAttributes: htmlAttributes,
|
|
arch: arch
|
|
});
|
|
|
|
if (! memoizedBoilerplate[memHash]) {
|
|
memoizedBoilerplate[memHash] = boilerplateByArch[arch].toHTML({
|
|
htmlAttributes: htmlAttributes
|
|
});
|
|
}
|
|
return memoizedBoilerplate[memHash];
|
|
}
|
|
|
|
var boilerplateOptions = _.extend({
|
|
htmlAttributes: htmlAttributes
|
|
}, _.pick(request, 'dynamicHead', 'dynamicBody'));
|
|
|
|
return boilerplateByArch[arch].toHTML(boilerplateOptions);
|
|
};
|
|
|
|
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 path.join(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 = parseurl(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") {
|
|
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);
|
|
};
|
|
|
|
var runWebAppServer = function () {
|
|
var shuttingDown = false;
|
|
var syncQueue = new Meteor._SynchronousQueue();
|
|
|
|
var getItemPathname = function (itemUrl) {
|
|
return decodeURIComponent(url.parse(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 = path.join(__meteor_bootstrap__.serverDir,
|
|
clientPath);
|
|
var clientDir = path.dirname(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: path.join(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: path.join(clientDir, item.sourceMap),
|
|
cacheable: true
|
|
};
|
|
}
|
|
}
|
|
});
|
|
|
|
var program = {
|
|
format: "web-program-pre1",
|
|
manifest: manifest,
|
|
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] = path.dirname(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) {
|
|
Fiber(function () {
|
|
WebAppInternals.staticFilesMiddleware(staticFiles, req, res, next);
|
|
}).run();
|
|
});
|
|
|
|
// 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) {
|
|
Fiber(function () {
|
|
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 undefined;
|
|
}
|
|
|
|
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 undefined;
|
|
}
|
|
|
|
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 undefined;
|
|
}
|
|
|
|
// /packages/asdfsad ... /__cordova/dafsdf.js
|
|
var pathname = parseurl(req).pathname;
|
|
var archKey = pathname.split('/')[1];
|
|
var archKeyCleaned = 'web.' + archKey.replace(/^__/, '');
|
|
|
|
if (!/^__/.test(archKey) || !_.has(archPath, archKeyCleaned)) {
|
|
archKey = WebApp.defaultArch;
|
|
} else {
|
|
archKey = archKeyCleaned;
|
|
}
|
|
|
|
var boilerplate;
|
|
try {
|
|
boilerplate = getBoilerplate(request, archKey);
|
|
} catch (e) {
|
|
Log.error("Error running template: " + e.stack);
|
|
res.writeHead(500, headers);
|
|
res.end();
|
|
return undefined;
|
|
}
|
|
|
|
var statusCode = res.statusCode ? res.statusCode : 200;
|
|
res.writeHead(statusCode, headers);
|
|
res.write(boilerplate);
|
|
res.end();
|
|
return undefined;
|
|
}).run();
|
|
});
|
|
|
|
// Return 404 by default, if no other handlers serve this URL.
|
|
app.use(function (req, res) {
|
|
res.writeHead(404);
|
|
res.end();
|
|
});
|
|
|
|
|
|
var httpServer = http.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,
|
|
// 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.
|
|
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;
|