mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
Replace underscore where easy and feasible & other minor modernization that I came across.
284 lines
8.2 KiB
JavaScript
284 lines
8.2 KiB
JavaScript
var Anser = require("anser");
|
|
var runLog = require('./run-log.js');
|
|
|
|
// options: listenPort, proxyToPort, proxyToHost,
|
|
// onFailure, ignoredUrls
|
|
var Proxy = function (options) {
|
|
var self = this;
|
|
|
|
self.listenPort = options.listenPort;
|
|
self.listenHost = options.listenHost;
|
|
// note: run-all.js updates proxyToPort directly
|
|
self.proxyToPort = options.proxyToPort;
|
|
self.proxyToHost = options.proxyToHost || '127.0.0.1';
|
|
self.onFailure = options.onFailure || function () {};
|
|
self.ignoredUrls = options.ignoredUrls || [];
|
|
|
|
self.mode = "hold";
|
|
self.httpQueue = []; // keys: req, res
|
|
self.websocketQueue = []; // keys: req, socket, head
|
|
|
|
self.proxy = null;
|
|
self.server = null;
|
|
};
|
|
|
|
Object.assign(Proxy.prototype, {
|
|
// Start the proxy server, block (yield) until it is ready to go
|
|
// (actively listening on outer and proxying to inner), and then
|
|
// return.
|
|
start: function () {
|
|
var self = this;
|
|
|
|
if (self.server) {
|
|
throw new Error("already running?");
|
|
}
|
|
|
|
self.started = false;
|
|
|
|
var http = require('http');
|
|
var net = require('net');
|
|
var httpProxy = require('http-proxy');
|
|
|
|
self.proxy = httpProxy.createProxyServer({
|
|
// agent is required to handle keep-alive, and http-proxy 1.0 is a little
|
|
// buggy without it: https://github.com/nodejitsu/node-http-proxy/pull/488
|
|
agent: new http.Agent({ maxSockets: 100 }),
|
|
xfwd: true
|
|
});
|
|
|
|
var server = self.server = http.createServer(function (req, res) {
|
|
// Normal HTTP request
|
|
if (self.ignoredUrls.includes(req.url)) {
|
|
return;
|
|
}
|
|
|
|
self.httpQueue.push({ req: req, res: res });
|
|
self._tryHandleConnections();
|
|
});
|
|
|
|
self.server.on('upgrade', function (req, socket, head) {
|
|
if (self.ignoredUrls.includes(req.url)) {
|
|
return;
|
|
}
|
|
|
|
// Websocket connection
|
|
self.websocketQueue.push({ req: req, socket: socket, head: head });
|
|
self._tryHandleConnections();
|
|
});
|
|
|
|
var allowStart;
|
|
var promise = new Promise(function (resolve) {
|
|
allowStart = resolve;
|
|
});
|
|
|
|
self.server.on('error', function (err) {
|
|
if (err.code === 'EADDRINUSE') {
|
|
var port = self.listenPort;
|
|
runLog.log(
|
|
"Can't listen on port " + port + ". Perhaps another Meteor is running?\n" +
|
|
"\n" +
|
|
"Running two copies of Meteor in the same application directory\n" +
|
|
"will not work. If something else is using port " + port + ", you can\n" +
|
|
"specify an alternative port with --port <port>.");
|
|
} else if (self.listenHost &&
|
|
(err.code === 'ENOTFOUND' || err.code === 'EADDRNOTAVAIL')) {
|
|
// This handles the case of "entered a DNS name that's unknown"
|
|
// (ENOTFOUND from getaddrinfo) and "entered some random IP that we
|
|
// can't bind to" (EADDRNOTAVAIL from listen).
|
|
runLog.log(
|
|
"Can't listen on host " + self.listenHost + " (" + err.code + " from " +
|
|
err.syscall + ").");
|
|
|
|
} else {
|
|
runLog.log('' + err);
|
|
}
|
|
self.onFailure();
|
|
allowStart();
|
|
});
|
|
|
|
// Don't crash if the app doesn't respond. instead return an error
|
|
// immediately. This shouldn't happen much since we try to not
|
|
// send requests if the app is down.
|
|
//
|
|
// Currently, this error is emitted if the proxy->server connection has an
|
|
// error (whether in HTTP or websocket proxying). It is not emitted if the
|
|
// client->proxy connection has an error, though this may change; see
|
|
// discussion at https://github.com/nodejitsu/node-http-proxy/pull/488
|
|
self.proxy.on('error', function (err, req, resOrSocket) {
|
|
if (err.code === 'HPE_HEADER_OVERFLOW') {
|
|
const logMessage = 'Error during proxy to server communication ' +
|
|
'due to the header size exceeding Node\'s currently ' +
|
|
'configured limit. This limit is configurable with a command ' +
|
|
'line option (https://nodejs.org/api/cli.html#cli_max_http_header_size_size ' +
|
|
'and https://docs.meteor.com/commandline.html#meteorrun).';
|
|
runLog.log(logMessage);
|
|
}
|
|
|
|
if (resOrSocket instanceof http.ServerResponse) {
|
|
if (!resOrSocket.headersSent) {
|
|
// Return a 503, but only if we haven't already written headers (or
|
|
// we'll get an ugly crash about rendering headers twice). end()
|
|
// doesn't crash if called twice so we don't have to conditionalize
|
|
// that call.
|
|
resOrSocket.writeHead(503, {
|
|
'Content-Type': 'text/plain'
|
|
});
|
|
}
|
|
resOrSocket.end('Unexpected error.');
|
|
} else if (resOrSocket instanceof net.Socket) {
|
|
resOrSocket.end();
|
|
}
|
|
});
|
|
|
|
self.server.listen(self.listenPort, self.listenHost || '0.0.0.0', function () {
|
|
if (self.server) {
|
|
self.started = true;
|
|
} else {
|
|
// stop() got called while we were invoking listen! Close the server (we
|
|
// still have the var server). The rest of the cleanup shouldn't be
|
|
// necessary.
|
|
server.close();
|
|
}
|
|
allowStart();
|
|
});
|
|
|
|
promise.await();
|
|
},
|
|
|
|
// Idempotent.
|
|
stop: function () {
|
|
var self = this;
|
|
|
|
if (! self.server) {
|
|
return;
|
|
}
|
|
|
|
if (! self.started) {
|
|
// This probably means that we failed to listen. However, there could be a
|
|
// race condition and we could be in the middle of starting to listen! In
|
|
// that case, the listen callback will notice that we nulled out server
|
|
// here.
|
|
self.server = null;
|
|
return;
|
|
}
|
|
|
|
// This stops listening but allows existing connections to
|
|
// complete gracefully.
|
|
self.server.close();
|
|
self.server = null;
|
|
|
|
// It doesn't seem to be necessary to do anything special to
|
|
// destroy an httpProxy proxyserver object.
|
|
self.proxy = null;
|
|
|
|
// Drop any held connections.
|
|
self.httpQueue?.forEach(function (c) {
|
|
c.res.statusCode = 500;
|
|
c.res.end();
|
|
});
|
|
self.httpQueue = [];
|
|
|
|
self.websocketQueue?.forEach(function (c) {
|
|
c.socket.destroy();
|
|
});
|
|
self.websocketQueue = [];
|
|
|
|
self.mode = "hold";
|
|
},
|
|
|
|
_tryHandleConnections: function () {
|
|
var self = this;
|
|
|
|
function attempt(resOrSocket, fn) {
|
|
try {
|
|
return fn();
|
|
} catch (e) {
|
|
if (typeof resOrSocket.writeHead === "function") {
|
|
resOrSocket.writeHead(400, {
|
|
'Content-Type': 'text/plain'
|
|
});
|
|
}
|
|
resOrSocket.end("Bad request\n");
|
|
}
|
|
}
|
|
|
|
while (self.httpQueue.length) {
|
|
if (self.mode !== "errorpage" && self.mode !== "proxy") {
|
|
break;
|
|
}
|
|
|
|
var c = self.httpQueue.shift();
|
|
if (self.mode === "errorpage") {
|
|
showErrorPage(c.res);
|
|
} else {
|
|
attempt(c.res, () => self.proxy.web(c.req, c.res, {
|
|
target: 'http://' + self.proxyToHost + ':' + self.proxyToPort
|
|
}));
|
|
}
|
|
}
|
|
|
|
while (self.websocketQueue.length) {
|
|
if (self.mode !== "proxy") {
|
|
break;
|
|
}
|
|
|
|
var c = self.websocketQueue.shift();
|
|
attempt(c.socket, () => self.proxy.ws(c.req, c.socket, c.head, {
|
|
target: 'http://' + self.proxyToHost + ':' + self.proxyToPort
|
|
}));
|
|
}
|
|
},
|
|
|
|
// The proxy can be in one of three modes:
|
|
// - "hold": hold connections until the mode changes
|
|
// - "proxy": connections are proxied to the configured port
|
|
// - "errorpage": an error page is served to HTTP connections, and
|
|
// websocket connections are held
|
|
//
|
|
// The initial mode is "hold".
|
|
setMode: function (mode) {
|
|
var self = this;
|
|
self.mode = mode;
|
|
self._tryHandleConnections();
|
|
}
|
|
});
|
|
|
|
function showErrorPage(res) {
|
|
// XXX serve an app that shows the logs nicely and that also
|
|
// knows how to reload when the server comes back up
|
|
res.writeHead(200, {'Content-Type': 'text/html'});
|
|
res.write(`
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>App crashing</title>
|
|
<style type='text/css'>
|
|
body { margin: 0; }
|
|
h3 {
|
|
margin: 0;
|
|
font-family: sans-serif;
|
|
padding: 20px 10px 10px 10px;
|
|
background: #eee;
|
|
}
|
|
pre { margin: 20px; }
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
<h3>Your app is crashing. Here's the latest log:</h3>
|
|
|
|
<pre>`);
|
|
|
|
runLog.getLog().forEach(function (item) {
|
|
res.write(Anser.ansiToHtml(Anser.escapeForHtml(item.message)) + "\n");
|
|
});
|
|
|
|
res.write(`</pre>
|
|
</body>
|
|
</html>`)
|
|
|
|
res.end();
|
|
}
|
|
|
|
exports.Proxy = Proxy;
|