mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
If an app is trying to connect to a proxy that's down, this prevents it from freezing up and allows it to try another proxy if it hears about one.
588 lines
20 KiB
JavaScript
588 lines
20 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 optimist = Npm.require('optimist');
|
|
var useragent = Npm.require('useragent');
|
|
var send = Npm.require('send');
|
|
|
|
WebApp = {};
|
|
WebAppInternals = {};
|
|
|
|
var findGalaxy = _.once(function () {
|
|
if (!('GALAXY' in process.env)) {
|
|
console.log(
|
|
"To do Meteor Galaxy operations like binding to a Galaxy " +
|
|
"proxy, the GALAXY environment variable must be set.");
|
|
process.exit(1);
|
|
}
|
|
|
|
return DDP.connect(process.env['GALAXY']);
|
|
});
|
|
|
|
// 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');
|
|
};
|
|
|
|
// #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 (req) {
|
|
var userAgent = useragent.lookup(req.headers['user-agent']);
|
|
return {
|
|
name: camelCase(userAgent.family),
|
|
major: +userAgent.major,
|
|
minor: +userAgent.minor,
|
|
patch: +userAgent.patch
|
|
};
|
|
};
|
|
|
|
WebApp.categorizeRequest = function (req) {
|
|
return {
|
|
browser: identifyBrowser(req),
|
|
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 htmlAttributes = function (template, request) {
|
|
var attributes = '';
|
|
_.each(htmlAttributeHooks || [], function (hook) {
|
|
var attribute = hook(request);
|
|
if (attribute !== null && attribute !== undefined && attribute !== '')
|
|
attributes += ' ' + attribute;
|
|
});
|
|
return template.replace('##HTML_ATTRIBUTES##', attributes);
|
|
};
|
|
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;
|
|
};
|
|
|
|
// This is used to move legacy environment variables into deployConfig, where
|
|
// other packages look for them. We probably don't want it here forever.
|
|
var copyEnvVarToDeployConfig = function (deployConfig, envVar,
|
|
packageName, configKey) {
|
|
if (process.env[envVar]) {
|
|
if (! deployConfig.packages[packageName])
|
|
deployConfig.packages[packageName] = {};
|
|
deployConfig.packages[packageName][configKey] = process.env[envVar];
|
|
}
|
|
};
|
|
|
|
var runWebAppServer = function () {
|
|
// read the control for the client we'll be serving up
|
|
var clientJsonPath = path.join(__meteor_bootstrap__.serverDir,
|
|
__meteor_bootstrap__.configJson.client);
|
|
var clientDir = path.dirname(clientJsonPath);
|
|
var clientJson = JSON.parse(fs.readFileSync(clientJsonPath, 'utf8'));
|
|
|
|
if (clientJson.format !== "browser-program-pre1")
|
|
throw new Error("Unsupported format for client assets: " +
|
|
JSON.stringify(clientJson.format));
|
|
|
|
// XXX change all this config to something more reasonable.
|
|
// and move it out of webapp into a different package so you don't
|
|
// have weird things like mongo-livedata weak-dep'ing on webapp
|
|
var deployConfig =
|
|
process.env.METEOR_DEPLOY_CONFIG
|
|
? JSON.parse(process.env.METEOR_DEPLOY_CONFIG) : {};
|
|
if (!deployConfig.packages)
|
|
deployConfig.packages = {};
|
|
if (!deployConfig.boot)
|
|
deployConfig.boot = {};
|
|
if (!deployConfig.boot.bind)
|
|
deployConfig.boot.bind = {};
|
|
|
|
// check environment for legacy env variables.
|
|
if (process.env.PORT && !_.has(deployConfig.boot.bind, 'localPort')) {
|
|
deployConfig.boot.bind.localPort = parseInt(process.env.PORT);
|
|
}
|
|
if (process.env.BIND_IP && !_.has(deployConfig.boot.bind, 'localIp')) {
|
|
deployConfig.boot.bind.localIp = process.env.BIND_IP;
|
|
}
|
|
copyEnvVarToDeployConfig(deployConfig, "MONGO_URL", "mongo-livedata", "url");
|
|
|
|
// webserver
|
|
var app = connect();
|
|
|
|
// 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());
|
|
|
|
// Auto-compress any json, javascript, or text.
|
|
app.use(connect.compress());
|
|
|
|
var staticFiles = {};
|
|
_.each(clientJson.manifest, function (item) {
|
|
if (item.url && item.where === "client") {
|
|
staticFiles[url.parse(item.url).pathname] = {
|
|
path: item.path,
|
|
cacheable: item.cacheable,
|
|
// Link from source to its map
|
|
sourceMapUrl: item.sourceMapUrl
|
|
};
|
|
|
|
if (item.sourceMap) {
|
|
// Serve the source map too, under the specified URL. We assume all
|
|
// source maps are cacheable.
|
|
staticFiles[url.parse(item.sourceMapUrl).pathname] = {
|
|
path: item.sourceMap,
|
|
cacheable: true
|
|
};
|
|
}
|
|
}
|
|
});
|
|
|
|
// Serve static files from the manifest.
|
|
// This is inspired by the 'static' middleware.
|
|
app.use(function (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;
|
|
}
|
|
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);
|
|
|
|
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);
|
|
});
|
|
|
|
// 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");
|
|
});
|
|
|
|
// Will be updated by main before we listen.
|
|
var boilerplateHtml = null;
|
|
app.use(function (req, res, next) {
|
|
if (! appUrl(req.url))
|
|
return next();
|
|
|
|
if (!boilerplateHtml)
|
|
throw new Error("boilerplateHtml should be set before listening!");
|
|
|
|
var request = WebApp.categorizeRequest(req);
|
|
|
|
res.writeHead(200, {'Content-Type': 'text/html; charset=utf-8'});
|
|
|
|
var requestSpecificHtml = htmlAttributes(boilerplateHtml, request);
|
|
res.write(requestSpecificHtml);
|
|
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 = [];
|
|
|
|
// start up app
|
|
_.extend(WebApp, {
|
|
connectHandlers: packageAndAppHandlers,
|
|
httpServer: httpServer,
|
|
// metadata about the client program that we serve
|
|
clientProgram: {
|
|
manifest: clientJson.manifest
|
|
// XXX do we need a "root: clientDir" field here? it used to be here but
|
|
// was unused.
|
|
},
|
|
// 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
|
|
});
|
|
// XXX move deployConfig out of __meteor_bootstrap__, after deciding where in
|
|
// the world it goes. maybe a new deploy-config package?
|
|
_.extend(__meteor_bootstrap__, {
|
|
deployConfig: deployConfig
|
|
});
|
|
|
|
// 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) {
|
|
argv = optimist(argv).boolean('keepalive').argv;
|
|
|
|
var boilerplateHtmlPath = path.join(clientDir, clientJson.page);
|
|
boilerplateHtml =
|
|
fs.readFileSync(boilerplateHtmlPath, 'utf8')
|
|
.replace(
|
|
"// ##RUNTIME_CONFIG##",
|
|
"__meteor_runtime_config__ = " +
|
|
JSON.stringify(__meteor_runtime_config__) + ";")
|
|
.replace(
|
|
/##ROOT_URL_PATH_PREFIX##/g,
|
|
__meteor_runtime_config__.ROOT_URL_PATH_PREFIX || "");
|
|
|
|
// only start listening after all the startup code has run.
|
|
var bind = deployConfig.boot.bind;
|
|
var localPort = bind.localPort || 0;
|
|
var localIp = bind.localIp || '0.0.0.0';
|
|
httpServer.listen(localPort, localIp, Meteor.bindEnvironment(function() {
|
|
if (argv.keepalive || true)
|
|
console.log("LISTENING"); // must match run.js
|
|
var port = httpServer.address().port;
|
|
if (bind.viaProxy && bind.viaProxy.proxyEndpoint) {
|
|
WebAppInternals.bindToProxy(bind.viaProxy);
|
|
} else if (bind.viaProxy) {
|
|
// bind via the proxy, but we'll have to find it ourselves via
|
|
// ultraworld.
|
|
var galaxy = findGalaxy();
|
|
var proxyServiceName = deployConfig.proxyServiceName || "proxy";
|
|
galaxy.subscribe('servicesByName', proxyServiceName);
|
|
var Proxies = new Meteor.Collection('services', {
|
|
manager: galaxy
|
|
});
|
|
var doBinding = function (proxyService) {
|
|
if (proxyService.providers.proxy) {
|
|
Log("Attempting to bind to proxy at " + proxyService.providers.proxy);
|
|
WebAppInternals.bindToProxy(_.extend({
|
|
proxyEndpoint: proxyService.providers.proxy
|
|
}, bind.viaProxy));
|
|
}
|
|
};
|
|
Proxies.find().observe({
|
|
added: doBinding,
|
|
changed: doBinding
|
|
});
|
|
}
|
|
|
|
var callbacks = onListeningCallbacks;
|
|
onListeningCallbacks = null;
|
|
_.each(callbacks, function (x) { x(); });
|
|
}, function (e) {
|
|
console.error("Error listening:", e);
|
|
console.error(e.stack);
|
|
}));
|
|
|
|
if (argv.keepalive)
|
|
initKeepalive();
|
|
return 'DAEMON';
|
|
};
|
|
};
|
|
|
|
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");
|
|
// XXX move these into deployConfig?
|
|
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.
|
|
var pid = {
|
|
job: process.env.GALAXY_JOB,
|
|
lastStarted: process.env.LAST_START,
|
|
app: process.env.GALAXY_APP
|
|
};
|
|
var myHost = os.hostname();
|
|
|
|
var ddpBindTo = {
|
|
ddpUrl: 'ddp://' + proxyConfig.bindHost + ':' + securePort + bindPathPrefix + '/',
|
|
insecurePort: insecurePort
|
|
};
|
|
|
|
// This is run after packages are loaded (in main) so we can use
|
|
// DDP.connect.
|
|
var proxy = DDP.connect(proxyConfig.proxyEndpoint);
|
|
var route = process.env.ROUTE;
|
|
var host = route.split(":")[0];
|
|
var port = +route.split(":")[1];
|
|
|
|
var completedBindings = {
|
|
ddp: false,
|
|
http: false,
|
|
https: proxyConfig.securePort !== null ? false : undefined
|
|
};
|
|
|
|
var bindingDoneCallback = function (binding) {
|
|
return function (err, resp) {
|
|
if (err)
|
|
throw err;
|
|
|
|
completedBindings[binding] = true;
|
|
var completedAll = _.every(_.keys(completedBindings), function (binding) {
|
|
return (completedBindings[binding] ||
|
|
completedBindings[binding] === undefined);
|
|
});
|
|
if (completedAll)
|
|
Log("Bound to proxy.");
|
|
return completedAll;
|
|
};
|
|
};
|
|
|
|
proxy.call('bindDdp', {
|
|
pid: pid,
|
|
bindTo: ddpBindTo,
|
|
proxyTo: {
|
|
host: host,
|
|
port: port,
|
|
pathPrefix: bindPathPrefix + '/websocket'
|
|
}
|
|
}, bindingDoneCallback("ddp"));
|
|
proxy.call('bindHttp', {
|
|
pid: pid,
|
|
bindTo: {
|
|
host: proxyConfig.bindHost,
|
|
port: insecurePort,
|
|
pathPrefix: bindPathPrefix
|
|
},
|
|
proxyTo: {
|
|
host: host,
|
|
port: port,
|
|
pathPrefix: bindPathPrefix
|
|
}
|
|
}, bindingDoneCallback("http"));
|
|
if (proxyConfig.securePort !== null) {
|
|
proxy.call('bindHttp', {
|
|
pid: pid,
|
|
bindTo: {
|
|
host: proxyConfig.bindHost,
|
|
port: securePort,
|
|
pathPrefix: bindPathPrefix,
|
|
ssl: true
|
|
},
|
|
proxyTo: {
|
|
host: host,
|
|
port: port,
|
|
pathPrefix: bindPathPrefix
|
|
}
|
|
}, bindingDoneCallback("https"));
|
|
}
|
|
};
|
|
|
|
runWebAppServer();
|