mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
Fixes #3730. Testing Done: Manual testing based on the report in #3730. Also confirmed that `</script>` is not a problem. I would have added a test-packages test but there's no easy way to override Meteor.settings in test-packages. Bugs closed: 3730 Reviewed at https://rbcommons.com/s/meteor/r/1/
791 lines
27 KiB
JavaScript
791 lines
27 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 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 = {};
|
|
|
|
WebApp.defaultArch = 'web.browser';
|
|
|
|
// XXX maps archs to manifests
|
|
WebApp.clientPrograms = {};
|
|
|
|
// XXX maps archs to program path on filesystem
|
|
var archPath = {};
|
|
|
|
var bundledJsCssPrefix;
|
|
|
|
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 {
|
|
browser: identifyBrowser(req.headers['user-agent']),
|
|
url: url.parse(req.url, true)
|
|
};
|
|
};
|
|
|
|
// 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 is now called 'prefinish'.
|
|
// https://github.com/joyent/node/commit/7c9b6070
|
|
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. 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 htmlAttributes = getHtmlAttributes(request);
|
|
|
|
// The only thing that changes from request to request (for now) 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];
|
|
};
|
|
|
|
WebAppInternals.generateBoilerplateInstance = function (arch,
|
|
manifest,
|
|
additionalOptions) {
|
|
additionalOptions = additionalOptions || {};
|
|
|
|
var runtimeConfig = _.extend(
|
|
_.clone(__meteor_runtime_config__),
|
|
additionalOptions.runtimeConfigOverrides || {}
|
|
);
|
|
|
|
var jsCssPrefix;
|
|
if (arch === 'web.cordova') {
|
|
// in cordova we serve assets up directly from disk so it doesn't make
|
|
// sense to use the prefix (ordinarily something like a CDN) and go out
|
|
// to the internet for those files.
|
|
jsCssPrefix = '';
|
|
} else {
|
|
jsCssPrefix = bundledJsCssPrefix ||
|
|
__meteor_runtime_config__.ROOT_URL_PATH_PREFIX || '';
|
|
}
|
|
|
|
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 || '',
|
|
bundledJsCssPrefix: jsCssPrefix,
|
|
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) {
|
|
next();
|
|
return;
|
|
}
|
|
var pathname = connect.utils.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).
|
|
//
|
|
// We cache non-cacheable files anyway. This isn't really correct, as users
|
|
// can change the files and changes won't propagate immediately. However, if
|
|
// we don't cache them, browsers will 'flicker' when rerendering
|
|
// images. Eventually we will probably want to rewrite URLs of static assets
|
|
// to include a query parameter to bust caches. That way we can both get
|
|
// good caching behavior and allow users to change assets without delay.
|
|
// https://github.com/meteor/meteor/issues/773
|
|
var maxAge = info.cacheable
|
|
? 1000 * 60 * 60 * 24 * 365
|
|
: 1000 * 60 * 60 * 24;
|
|
|
|
// 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");
|
|
// XXX if it is a manifest we are serving, set additional headers
|
|
if (/\/manifest.json$/.test(pathname)) {
|
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
}
|
|
}
|
|
|
|
if (info.content) {
|
|
res.write(info.content);
|
|
res.end();
|
|
} else {
|
|
send(req, info.absolutePath)
|
|
.maxage(maxAge)
|
|
.hidden(true) // if we specified a dotfile in the manifest, serve it
|
|
.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\./, '');
|
|
};
|
|
|
|
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,
|
|
// 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 = {
|
|
manifest: manifest,
|
|
version: WebAppHashing.calculateClientHash(manifest, null, _.pick(
|
|
__meteor_runtime_config__, 'PUBLIC_SETTINGS')),
|
|
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[path.join(urlPrefix, 'manifest.json')] = {
|
|
content: JSON.stringify(program),
|
|
cacheable: true,
|
|
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?
|
|
WebAppInternals.refreshableAssets = {
|
|
allCss: boilerplateByArch[WebApp.defaultArch].baseData.css
|
|
};
|
|
});
|
|
};
|
|
|
|
WebAppInternals.reloadClientPrograms();
|
|
|
|
// webserver
|
|
var app = connect();
|
|
|
|
// Auto-compress any json, javascript, or text.
|
|
app.use(connect.compress());
|
|
|
|
// Packages and apps can add handlers that run before any other Meteor
|
|
// handlers via WebApp.rawConnectHandlers.
|
|
var rawConnectHandlers = connect();
|
|
app.use(rawConnectHandlers);
|
|
|
|
// 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) {
|
|
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.
|
|
headers['Content-Type'] = 'text/css; charset=utf-8';
|
|
res.writeHead(200, headers);
|
|
res.write(".meteor-css-not-found-error { width: 0px;}");
|
|
res.end();
|
|
return undefined;
|
|
}
|
|
|
|
// /packages/asdfsad ... /__cordova/dafsdf.js
|
|
var pathname = connect.utils.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);
|
|
res.writeHead(500, headers);
|
|
res.end();
|
|
return undefined;
|
|
}
|
|
|
|
res.writeHead(200, headers);
|
|
res.write(boilerplate);
|
|
res.end();
|
|
return undefined;
|
|
});
|
|
|
|
// 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);
|
|
|
|
|
|
// 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();
|
|
},
|
|
// Hack: allow http tests to call connect.basicAuth without making them
|
|
// Npm.depends on another copy of connect. (That would be fine if we could
|
|
// have test-only NPM dependencies but is overkill here.)
|
|
__basicAuth__: connect.basicAuth
|
|
});
|
|
|
|
// 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 = parseInt(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.setBundledJsCssPrefix = function (prefix) {
|
|
bundledJsCssPrefix = prefix;
|
|
WebAppInternals.generateBoilerplate();
|
|
};
|
|
|
|
// 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;
|