Files
meteor/packages/webapp/webapp_server.js
Matthew Arbesfeld 8bcbd65344 Separate "browser" target into web.browser/cordova
Cordova projects often have a different set of files than web targets,
so we would like to be able to target different client architectures in
our bundles. Ideally, we allow the user to use arbitrary client
architectures - but this patch is a step in the right direction by
abstracting out more of the hard coded "browser"/"os" lines.

We accomplish this separation in a backwards compatible way by allowing
api.___ commands to target a "client" architecture. For example,
api.addFiles('a.js', 'client') adds 'a.js' to both the 'client.browser'
and 'client.cordova' targets.

Effects on 0.9 packaging stuff: packages don't have to change, but the
"data.json" file in ".meteor0" has "browser" in some places. We think we
have to fix the troposphere code where this data.json is created.

Some plugins will also be backwards-incompatible with this change, since
many have a "clientArch.matches("browser")" line in the plugin
code. Ideally, we fix plugins so that this stops being an issue, but for
now package authors can just patch that line.

At the compiled (unipackage) level the new names are 'web.browser' and
'web.cordova', replacing 'browser'. In package.js, the new names are
'client.browser' and 'client.cordova', serving as an adjunct to 'client'.
2014-07-31 14:12:15 -07:00

1100 lines
38 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 SHORT_SOCKET_TIMEOUT = 5*1000;
var LONG_SOCKET_TIMEOUT = 120*1000;
WebApp = {};
WebAppInternals = {};
var bundledJsCssPrefix;
// Keepalives so that when the outer server dies unceremoniously and
// doesn't kill us, we quit ourselves. A little gross, but better than
// pidfiles.
// XXX This should really be part of the boot script, not the webapp package.
// Or we should just get rid of it, and rely on containerization.
var initKeepalive = function () {
var keepaliveCount = 0;
process.stdin.on('data', function (data) {
keepaliveCount = 0;
});
process.stdin.resume();
setInterval(function () {
keepaliveCount ++;
if (keepaliveCount >= 3) {
console.log("Failed to receive keepalive! Exiting.");
process.exit(1);
}
}, 3000);
};
var sha1 = function (contents) {
var hash = crypto.createHash('sha1');
hash.update(contents);
return hash.digest('hex');
};
var readUtf8FileSync = function (filename) {
return Future.wrap(fs.readFile)(filename, 'utf8').wait();
};
// #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 a string,
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;
};
// Calculate a hash of all the client resources downloaded by the
// browser, including the application HTML, runtime config, code, and
// static files.
//
// This hash *must* change if any resources seen by the browser
// change, and ideally *doesn't* change for any server-only changes
// (but the second is a performance enhancement, not a hard
// requirement).
var calculateClientHash = function (includeFilter) {
var hash = crypto.createHash('sha1');
// Omit the old hashed client values in the new hash. These may be
// modified in the new boilerplate.
hash.update(JSON.stringify(_.omit(__meteor_runtime_config__,
['autoupdateVersion', 'autoupdateVersionRefreshable']), 'utf8'));
_.each(WebApp.clientProgram.manifest, function (resource) {
if ((! includeFilter || includeFilter(resource.type)) &&
(resource.where === 'client' || resource.where === 'internal')) {
hash.update(resource.path);
hash.update(resource.hash);
}
});
return hash.digest('hex');
};
// 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 () {
WebApp.clientHash = calculateClientHash();
WebApp.calculateClientHashRefreshable = function () {
return calculateClientHash(function (name) {
return name === "css";
});
};
WebApp.calculateClientHashNonRefreshable = function () {
return calculateClientHash(function (name) {
return name !== "css";
});
};
});
// 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.
var boilerplateFunc = null;
var boilerplateBaseData = null;
var memoizedBoilerplate = {};
// 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.
var getBoilerplate = function (request) {
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 boilerplateKey = JSON.stringify({
inlineScriptsAllowed: inlineScriptsAllowed,
htmlAttributes: htmlAttributes
});
if (! _.has(memoizedBoilerplate, boilerplateKey)) {
var boilerplateData = _.extend({
htmlAttributes: htmlAttributes,
inlineScriptsAllowed: WebAppInternals.inlineScriptsAllowed()
}, boilerplateBaseData);
memoizedBoilerplate[boilerplateKey] = "<!DOCTYPE html>\n" +
Blaze.toHTML(Blaze.With(boilerplateData, boilerplateFunc));
}
return memoizedBoilerplate[boilerplateKey];
};
// Serve static files from the manifest or added with
// `addStaticJs`. Exported for tests.
// Options are:
// - staticFiles: object mapping pathname of file in manifest -> {
// path, cacheable, sourceMapUrl, type }
// - clientDir: root directory for static files from client manifest
WebAppInternals.staticFilesMiddleware = function (options, req, res, next) {
if ('GET' != req.method && 'HEAD' != req.method) {
next();
return;
}
var pathname = connect.utils.parseUrl(req).pathname;
var staticFiles = options.staticFiles;
var clientDir = options.clientDir;
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 understands.
// (The files also contain '//#' comments which FF 24 understands and
// Chrome doesn't understand yet.)
//
// Eventually we should set the SourceMap header but the current version of
// Chrome and no version of FF supports it.
//
// To figure out if your version of Chrome should support the SourceMap
// header,
// - go to chrome://version. Let's say the Chrome version is
// 28.0.1500.71 and the Blink version is 537.36 (@153022)
// - go to http://src.chromium.org/viewvc/blink/branches/chromium/1500/Source/core/inspector/InspectorPageAgent.cpp?view=log
// where the "1500" is the third part of your Chrome version
// - find the first revision that is no greater than the "153022"
// number. That's probably the first one and it probably has
// a message of the form "Branch 1500 - blink@r149738"
// - If *that* revision number (149738) is at least 151755,
// then Chrome should support SourceMap (not just X-SourceMap)
// (The change is https://codereview.chromium.org/15832007)
//
// You also need to enable source maps in Chrome: open dev tools, click
// the gear in the bottom right corner, and select "enable source maps".
//
// Firefox 23+ supports source maps but doesn't support either header yet,
// so we include the '//#' comment for it:
// https://bugzilla.mozilla.org/show_bug.cgi?id=765993
// In FF 23 you need to turn on `devtools.debugger.source-maps-enabled`
// in `about:config` (it is on by default in FF 24).
if (info.sourceMapUrl)
res.setHeader('X-SourceMap', 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");
}
send(req, path.join(clientDir, info.path))
.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.path);
res.writeHead(500);
res.end();
})
.pipe(res);
};
var runWebAppServer = function () {
var shuttingDown = false;
var syncQueue = new Meteor._SynchronousQueue();
var getItemPathname = function (itemUrl) {
return decodeURIComponent(url.parse(itemUrl).pathname);
};
var staticFiles;
var clientJsonPath;
var clientDir;
var clientJson;
WebAppInternals.reloadClientProgram = function () {
syncQueue.runTask(function() {
try {
// read the control for the client we'll be serving up
clientJsonPath = path.join(__meteor_bootstrap__.serverDir,
__meteor_bootstrap__.configJson.client);
clientDir = path.dirname(clientJsonPath);
clientJson = JSON.parse(readUtf8FileSync(clientJsonPath));
if (clientJson.format !== "web-program-pre1")
throw new Error("Unsupported format for client assets: " +
JSON.stringify(clientJson.format));
staticFiles = {};
_.each(clientJson.manifest, function (item) {
if (item.url && item.where === "client") {
staticFiles[getItemPathname(item.url)] = {
path: 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[getItemPathname(item.sourceMapUrl)] = {
path: item.sourceMap,
cacheable: true
};
}
}
});
WebApp.clientProgram = {
manifest: clientJson.manifest
// XXX do we need a "root: clientDir" field here? it used to be here but
// was unused.
};
// Exported for tests.
WebAppInternals.staticFiles = staticFiles;
} catch (e) {
Log.error("Error reloading the client program: " + e.message);
process.exit(1);
}
});
};
WebAppInternals.reloadClientProgram();
if (! clientJsonPath || ! clientDir || ! clientJson)
throw new Error("Client config file not parsed.");
// 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);
// 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) {
return WebAppInternals.staticFilesMiddleware({
staticFiles: staticFiles,
clientDir: clientDir
}, 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) {
if (! appUrl(req.url))
return next();
if (!boilerplateFunc)
throw new Error("boilerplateFunc should be set before listening!");
if (!boilerplateBaseData)
throw new Error("boilerplateBaseData should be set before listening!");
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;
}
var boilerplate;
try {
boilerplate = getBoilerplate(request);
} 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);
// For now, handle SIGHUP here. Later, this should be in some centralized
// Meteor shutdown code.
process.on('SIGHUP', Meteor.bindEnvironment(function () {
shuttingDown = true;
// tell others with websockets open that we plan to close this.
// XXX: Eventually, this should be done with a standard meteor shut-down
// logic path.
httpServer.emit('meteor-closing');
httpServer.close(Meteor.bindEnvironment(function () {
if (proxy) {
try {
proxy.call('removeBindingsForJob', process.env.GALAXY_JOB);
} catch (e) {
Log.error("Error removing bindings: " + e.message);
process.exit(1);
}
}
process.exit(0);
}, "On http server close failed"));
// Ideally we will close before this hits.
Meteor.setTimeout(function () {
Log.warn("Closed by SIGHUP but one or more HTTP requests may not have finished.");
process.exit(1);
}, 5000);
}, function (err) {
console.log(err);
process.exit(1);
}));
// 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) {
// main happens post startup hooks, so we don't need a Meteor.startup() to
// ensure this happens after the galaxy package is loaded.
var AppConfig = Package["application-configuration"].AppConfig;
// We used to use the optimist npm package to parse argv here, but it's
// overkill (and no longer in the dev bundle). Just assume any instance of
// '--keepalive' is a use of the option.
var expectKeepalives = _.contains(argv, '--keepalive');
var boilerplateTemplateSource = Assets.getText("boilerplate.html");
// Exported to allow client-side only changes to rebuild the boilerplate
// without requiring a full server restart.
WebAppInternals.generateBoilerplate = function () {
syncQueue.runTask(function() {
boilerplateBaseData = {
// 'htmlAttributes' and 'inlineScriptsAllowed' are set at render
// time, because they are allowed to change from request to
// request.
css: [],
js: [],
head: '',
body: '',
additionalStaticJs: _.map(
additionalStaticJs,
function (contents, pathname) {
return {
pathname: pathname,
contents: contents
};
}
),
meteorRuntimeConfig: JSON.stringify(__meteor_runtime_config__),
rootUrlPathPrefix: __meteor_runtime_config__.ROOT_URL_PATH_PREFIX || '',
bundledJsCssPrefix: bundledJsCssPrefix ||
__meteor_runtime_config__.ROOT_URL_PATH_PREFIX || ''
};
_.each(WebApp.clientProgram.manifest, function (item) {
if (item.type === 'css' && item.where === 'client') {
boilerplateBaseData.css.push({url: item.url});
}
if (item.type === 'js' && item.where === 'client') {
boilerplateBaseData.js.push({url: item.url});
}
if (item.type === 'head') {
boilerplateBaseData.head =
readUtf8FileSync(path.join(clientDir, item.path));
}
if (item.type === 'body') {
boilerplateBaseData.body =
readUtf8FileSync(path.join(clientDir, item.path));
}
});
var boilerplateRenderCode = SpacebarsCompiler.compile(
boilerplateTemplateSource, { isBody: true });
// Note that we are actually depending on eval's local environment capture
// so that UI and HTML are visible to the eval'd code.
boilerplateFunc = eval(boilerplateRenderCode);
// Clear the memoized boilerplate cache.
memoizedBoilerplate = {};
WebAppInternals.refreshableAssets = { allCss: boilerplateBaseData.css };
});
};
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 (expectKeepalives)
console.log("LISTENING"); // must match run-app.js
var proxyBinding;
AppConfig.configurePackage('webapp', function (configuration) {
if (proxyBinding)
proxyBinding.stop();
if (configuration && configuration.proxy) {
// TODO: We got rid of the place where this checks the app's
// configuration, because this wants to be configured for some things
// on a per-job basis. Discuss w/ teammates.
proxyBinding = AppConfig.configureService(
"proxy",
"pre0",
function (proxyService) {
if (proxyService && ! _.isEmpty(proxyService)) {
var proxyConf;
// XXX Figure out a per-job way to specify bind location
// (besides hardcoding the location for ADMIN_APP jobs).
if (process.env.ADMIN_APP) {
var bindPathPrefix = "";
if (process.env.GALAXY_APP !== "panel") {
bindPathPrefix = "/" + bindPathPrefix +
encodeURIComponent(
process.env.GALAXY_APP
).replace(/\./g, '_');
}
proxyConf = {
bindHost: process.env.GALAXY_NAME,
bindPathPrefix: bindPathPrefix,
requiresAuth: true
};
} else {
proxyConf = configuration.proxy;
}
Log("Attempting to bind to proxy at " +
proxyService);
WebAppInternals.bindToProxy(_.extend({
proxyEndpoint: proxyService
}, proxyConf));
}
}
);
}
});
var callbacks = onListeningCallbacks;
onListeningCallbacks = null;
_.each(callbacks, function (x) { x(); });
}, function (e) {
console.error("Error listening:", e);
console.error(e && e.stack);
}));
if (expectKeepalives)
initKeepalive();
return 'DAEMON';
};
};
var proxy;
WebAppInternals.bindToProxy = function (proxyConfig) {
var securePort = proxyConfig.securePort || 4433;
var insecurePort = proxyConfig.insecurePort || 8080;
var bindPathPrefix = proxyConfig.bindPathPrefix || "";
// XXX also support galaxy-based lookup
if (!proxyConfig.proxyEndpoint)
throw new Error("missing proxyEndpoint");
if (!proxyConfig.bindHost)
throw new Error("missing bindHost");
if (!process.env.GALAXY_JOB)
throw new Error("missing $GALAXY_JOB");
if (!process.env.GALAXY_APP)
throw new Error("missing $GALAXY_APP");
if (!process.env.LAST_START)
throw new Error("missing $LAST_START");
// XXX rename pid argument to bindTo.
// XXX factor out into a 'getPid' function in a 'galaxy' package?
var pid = {
job: process.env.GALAXY_JOB,
lastStarted: +(process.env.LAST_START),
app: process.env.GALAXY_APP
};
var myHost = os.hostname();
WebAppInternals.usingDdpProxy = true;
// This is run after packages are loaded (in main) so we can use
// Follower.connect.
if (proxy) {
// XXX the concept here is that our configuration has changed and
// we have connected to an entirely new follower set, which does
// not have the state that we set up on the follower set that we
// were previously connected to, and so we need to recreate all of
// our bindings -- analogous to getting a SIGHUP and rereading
// your configuration file. so probably this should actually tear
// down the connection and make a whole new one, rather than
// hot-reconnecting to a different URL.
proxy.reconnect({
url: proxyConfig.proxyEndpoint
});
} else {
proxy = Package["follower-livedata"].Follower.connect(
proxyConfig.proxyEndpoint, {
group: "proxy"
}
);
}
var route = process.env.ROUTE;
var ourHost = route.split(":")[0];
var ourPort = +route.split(":")[1];
var outstanding = 0;
var startedAll = false;
var checkComplete = function () {
if (startedAll && ! outstanding)
Log("Bound to proxy.");
};
var makeCallback = function () {
outstanding++;
return function (err) {
if (err)
throw err;
outstanding--;
checkComplete();
};
};
// for now, have our (temporary) requiresAuth flag apply to all
// routes created by this process.
var requiresDdpAuth = !! proxyConfig.requiresAuth;
var requiresHttpAuth = (!! proxyConfig.requiresAuth) &&
(pid.app !== "panel" && pid.app !== "auth");
// XXX a current limitation is that we treat securePort and
// insecurePort as a global configuration parameter -- we assume
// that if the proxy wants us to ask for 8080 to get port 80 traffic
// on our default hostname, that's the same port that we would use
// to get traffic on some other hostname that our proxy listens
// for. Likewise, we assume that if the proxy can receive secure
// traffic for our domain, it can assume secure traffic for any
// domain! Hopefully this will get cleaned up before too long by
// pushing that logic into the proxy service, so we can just ask for
// port 80.
// XXX BUG: if our configuration changes, and bindPathPrefix
// changes, it appears that we will not remove the routes derived
// from the old bindPathPrefix from the proxy (until the process
// exits). It is not actually normal for bindPathPrefix to change,
// certainly not without a process restart for other reasons, but
// it'd be nice to fix.
_.each(routes, function (route) {
var parsedUrl = url.parse(route.url, /* parseQueryString */ false,
/* slashesDenoteHost aka workRight */ true);
if (parsedUrl.protocol || parsedUrl.port || parsedUrl.search)
throw new Error("Bad url");
parsedUrl.host = null;
parsedUrl.path = null;
if (! parsedUrl.hostname) {
parsedUrl.hostname = proxyConfig.bindHost;
if (! parsedUrl.pathname)
parsedUrl.pathname = "";
if (! parsedUrl.pathname.indexOf("/") !== 0) {
// Relative path
parsedUrl.pathname = bindPathPrefix + parsedUrl.pathname;
}
}
var version = "";
var AppConfig = Package["application-configuration"].AppConfig;
version = AppConfig.getStarForThisJob() || "";
var parsedDdpUrl = _.clone(parsedUrl);
parsedDdpUrl.protocol = "ddp";
// Node has a hardcoded list of protocols that get '://' instead
// of ':'. ddp needs to be added to that whitelist. Until then, we
// can set the undocumented attribute 'slashes' to get the right
// behavior. It's not clear whether than is by design or accident.
parsedDdpUrl.slashes = true;
parsedDdpUrl.port = '' + securePort;
var ddpUrl = url.format(parsedDdpUrl);
var proxyToHost, proxyToPort, proxyToPathPrefix;
if (! _.has(route, 'forwardTo')) {
proxyToHost = ourHost;
proxyToPort = ourPort;
proxyToPathPrefix = parsedUrl.pathname;
} else {
var parsedFwdUrl = url.parse(route.forwardTo, false, true);
if (! parsedFwdUrl.hostname || parsedFwdUrl.protocol)
throw new Error("Bad forward url");
proxyToHost = parsedFwdUrl.hostname;
proxyToPort = parseInt(parsedFwdUrl.port || "80");
proxyToPathPrefix = parsedFwdUrl.pathname || "";
}
if (route.ddp) {
proxy.call('bindDdp', {
pid: pid,
bindTo: {
ddpUrl: ddpUrl,
insecurePort: insecurePort
},
proxyTo: {
tags: [version],
host: proxyToHost,
port: proxyToPort,
pathPrefix: proxyToPathPrefix + '/websocket'
},
requiresAuth: requiresDdpAuth
}, makeCallback());
}
if (route.http) {
proxy.call('bindHttp', {
pid: pid,
bindTo: {
host: parsedUrl.hostname,
port: insecurePort,
pathPrefix: parsedUrl.pathname
},
proxyTo: {
tags: [version],
host: proxyToHost,
port: proxyToPort,
pathPrefix: proxyToPathPrefix
},
requiresAuth: requiresHttpAuth
}, makeCallback());
// Only make the secure binding if we've been told that the
// proxy knows how terminate secure connections for us (has an
// appropriate cert, can bind the necessary port..)
if (proxyConfig.securePort !== null) {
proxy.call('bindHttp', {
pid: pid,
bindTo: {
host: parsedUrl.hostname,
port: securePort,
pathPrefix: parsedUrl.pathname,
ssl: true
},
proxyTo: {
tags: [version],
host: proxyToHost,
port: proxyToPort,
pathPrefix: proxyToPathPrefix
},
requiresAuth: requiresHttpAuth
}, makeCallback());
}
}
});
startedAll = true;
checkComplete();
};
// (Internal, unsupported interface -- subject to change)
//
// Listen for HTTP and/or DDP traffic and route it somewhere. Only
// takes effect when using a proxy service.
//
// 'url' is the traffic that we want to route, interpreted relative to
// the default URL where this app has been told to serve itself. It
// may not have a scheme or port, but it may have a host and a path,
// and if no host is provided the path need not be absolute. The
// following cases are possible:
//
// //somehost.com
// All incoming traffic for 'somehost.com'
// //somehost.com/foo/bar
// All incoming traffic for 'somehost.com', but only when
// the first two path components are 'foo' and 'bar'.
// /foo/bar
// Incoming traffic on our default host, but only when the
// first two path components are 'foo' and 'bar'.
// foo/bar
// Incoming traffic on our default host, but only when the path
// starts with our default path prefix, followed by 'foo' and
// 'bar'.
//
// (Yes, these scheme-less URLs that start with '//' are legal URLs.)
//
// You can select either DDP traffic, HTTP traffic, or both. Both
// secure and insecure traffic will be gathered (assuming the proxy
// service is capable, eg, has appropriate certs and port mappings).
//
// With no 'forwardTo' option, the traffic is received by this process
// for service by the hooks in this 'webapp' package. The original URL
// is preserved (that is, if you bind "/a", and a user visits "/a/b",
// the app receives a request with a path of "/a/b", not a path of
// "/b").
//
// With 'forwardTo', the process is instead sent to some other remote
// host. The URL is adjusted by stripping the path components in 'url'
// and putting the path components in the 'forwardTo' URL in their
// place. For example, if you forward "//somehost/a" to
// "//otherhost/x", and the user types "//somehost/a/b" into their
// browser, then otherhost will receive a request with a Host header
// of "somehost" and a path of "/x/b".
//
// The routing continues until this process exits. For now, all of the
// routes must be set up ahead of time, before the initial
// registration with the proxy. Calling addRoute from the top level of
// your JS should do the trick.
//
// When multiple routes are present that match a given request, the
// most specific route wins. When routes with equal specificity are
// present, the proxy service will distribute the traffic between
// them.
//
// options may be:
// - ddp: if true, the default, include DDP traffic. This includes
// both secure and insecure traffic, and both websocket and sockjs
// transports.
// - http: if true, the default, include HTTP/HTTPS traffic.
// - forwardTo: if provided, should be a URL with a host, optional
// path and port, and no scheme (the scheme will be derived from the
// traffic type; for now it will always be a http or ws connection,
// never https or wss, but we could add a forwardSecure flag to
// re-encrypt).
var routes = [];
WebAppInternals.addRoute = function (url, options) {
options = _.extend({
ddp: true,
http: true
}, options || {});
if (proxy)
// In the future, lift this restriction
throw new Error("Too late to add routes");
routes.push(_.extend({ url: url }, options));
};
// Receive traffic on our default URL.
WebAppInternals.addRoute("");
runWebAppServer();
var inlineScriptsAllowed = true;
WebAppInternals.inlineScriptsAllowed = function () {
return inlineScriptsAllowed;
};
WebAppInternals.setInlineScriptsAllowed = function (value) {
inlineScriptsAllowed = value;
};
WebAppInternals.setBundledJsCssPrefix = function (prefix) {
bundledJsCssPrefix = prefix;
};
// 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;