#!/usr/bin/env node#!/usr/bin/env nodeThe Pult server manages assigning ports to .dev domains, which it serves
through a local DNS server and HTTP reverse-proxy.
npm install -g
pult-server
var spawn = require('child_process').spawn;
var http = require('http');
var path = require('path');
var fs = require('fs');
var dns = require('native-dns');
var httpProxy = require('http-proxy');
var package = require('../package.json');function log() {
console.log('[pult-server]', Array.prototype.join.call(arguments, ' '));
}var options = {
firstPort: 7001,
listenHost: '127.0.0.1'
};
var argv = process.argv.slice(2);
while (argv[0] && argv[0][0] == '-') {
switch (argv[0]) {--foreground, -f: Do not fork to the background. case '-f':
case '--foreground':
options.foreground = argv.shift();
break;--port, -p: Set the first port to assign to a .dev domain.
Defaults to 7001. case '-p':
case '--port':
options.firstPort = +argv.argv[1];
argv = argv.slice(2);
break;--listen, -l: Set the host to listen on (HTTP and DNS). Defaults to
127.0.0.1. case '-l':
case '--listen':
options.listenHost = argv[1];
argv = argv.slice(2);
break;--help, -h: Show help text. case '-h':
case '--help':
options.help = argv.shift();
break;
default:
log('unknown option', argv[0]);
process.exit(1);
}
}if (options.help) {
log('-f, --foreground Do not found to the background');
log('-p, --port "port" Set first port to assign');
log('-l, --listen "host" Set host to listen on');
process.exit();
}If not running as root, respawn the process with sudo to gain permissions
to listen on ports 80 (HTTP) and 53 (DNS).
if (process.getuid() != 0) {
log('respawning with sudo...');Replace node with absolute path to executed node.
var args = process.argv.slice(1);
args.unshift(process.execPath);
var respawn = spawn('sudo', args, { stdio: 'inherit' });Exit with the same code as the respawn.
respawn.on('exit', function(code, signal) {
process.exit(code);
});Don’t do anything else.
return;
}Fork to background unless -f was passed. Pass -f to the new process to
prevent an infinite fork loop.
if (!options.foreground) {
log('forking to background...');Insert -f at the beginning of the argument list.
var args = process.argv.slice(1);
args.splice(1, 0, '-f');
spawn(process.execPath, args, { stdio: 'ignore', detached: true }).unref();Don’t do anything else.
return;
}The ports object will hold all the port assignments for .dev domains, as
well as the next port to assign.
var ports = {
'pult.dev': 80,
next: options.firstPort, get: function(domain) {
while (!this[domain] && domain.indexOf('.') != -1)
domain = domain.slice(domain.indexOf('.') + 1);
return this[domain];
}
};Respond to A or ANY questions for domains with assigned ports with an
A record pointing to the host of the HTTP server. Respond with NXDOMAIN
for domains without assigned ports. Respond to all other questions with
NOTIMP.
var dnsServer = dns.createServer();
dnsServer.on('request', function(req, res) {
var name = req.question[0].name;
var type = dns.consts.QTYPE_TO_NAME[req.question[0].type];
if (type == 'A' || type == 'ANY') {
if (ports.get(name)) {
res.answer.push(dns.A({
name: name,
address: options.listenHost,
ttl: 600
}));
} else {
res.header.rcode = dns.consts.NAME_TO_RCODE.NOTFOUND;
}
} else {
res.header.rcode = dns.consts.NAME_TO_RCODE.NOTIMP;
}
res.send();
});var httpServer = http.createServer();
httpServer.proxy = httpProxy.createProxyServer({ xfwd: true });Write a JSON object to the HTTP response along with a X-Pult-Version
header and end the response.
function endJSON(status, obj) {
var json = JSON.stringify(obj);
this.writeHead(status, {
'X-Pult-Version': package.version,
'Content-Type': 'application/json',
'Content-Length': json.length
});
if (this.req.method != 'HEAD')
this.write(json);
this.end();
}httpServer.on('request', function(req, res) {Allow response methods to access the request.
res.req = req;Assign extra response methods.
res.endJSON = endJSON;Get host without trailing port.
var host = req.headers.host.split(':')[0]; if (host == 'pult.dev' || host == 'localhost') { if (req.method == 'DELETE' && req.url == '/') {Respond with the final state of the server.
res.endJSON(200, ports);
return onExit();
}For all other URLs, only GET and HEAD are supported.
if (req.method != 'GET' && req.method != 'HEAD')
return res.endJSON(405, { method: req.method });Browsers request favicon.ico automatically. Prevent it from becoming a
.dev domain.
if (req.url == '/favicon.ico')
return res.endJSON(404, { url: req.url });
var domain = req.url.slice(1); if (domain) {
domain += '.dev';
var port = {};
port[domain] = ports[domain] || (ports[domain] = ports.next++);
return res.endJSON(200, port);For browsers, serve an HTML status page with links to each domain. For all other clients, serve a JSON response of port assignments.
} else {
if (req.headers.accept && req.headers.accept.indexOf('text/html') == 0) {
res.writeHead(200, {
'Content-Type': 'text/html',
'Content-Length': httpServer.statusHTML.length
});
res.write(httpServer.statusHTML);
return res.end();
} else {
return res.endJSON(200, ports);
}
}If a port is assigned to the requested host, proxy the request to that
port on 127.0.0.1. If an error occurs when trying to proxy, respond
with 502. If there is no port assigned, respond with 503.
} else {
var port = ports.get(host);
if (port) {
httpServer.proxy.web(req, res, {
target: 'http://127.0.0.1:' + port
}, function(err) {
return res.endJSON(502, err);
});
} else {
return res.endJSON(503, { host: host });
}
}
});If a port is assigned to the requested host, proxy the upgrade request to the websocket on the port.
httpServer.on('upgrade', function(req, socket, head) {
var port = port.get(req.headers.host.split(':')[0]);
if (port) {
proxy.ws(req, socket, head, {
target: 'ws://127.0.0.1:' + port
}, function(err) {FIXME: Better error handling.
log(err.stack);
});
}FIXME: Handle no port being assigned.
});var statusPath = path.join(__dirname, '..', 'lib', 'status.html');
fs.readFile(statusPath, function(err, data) {
if (err) throw err;
httpServer.statusHTML = data;
startServers();
});function startServers() {
httpServer.listen(80, options.listenHost);
dnsServer.serve(53, options.listenHost);
registerDNSServer();
}Add the local DNS server to /etc/resolv.conf on Linux, /etc/resolver/dev
on OS X.
function registerDNSServer() {
var nameserverLine = 'nameserver ' + options.listenHost;
if (process.platform == 'darwin') {Make sure /etc/resolver exists.
fs.mkdir('/etc/resolver', function(err) {
log('creating /etc/resolver/dev...');
fs.writeFile('/etc/resolver/dev', nameserverLine, function(err) {
if (err) throw err;
});
});
} else {Only add the nameserver line if it isn’t already present in
/etc/resolv.conf.
fs.readFile('/etc/resolv.conf', { encoding: 'utf8' }, function(err, data) {
if (err) throw err;
if (data.indexOf(nameserverLine) == -1) {
log('adding', nameserverLine, 'to /etc/resolv.conf');
var newData = nameserverLine + '\n' + data;
fs.writeFile('/etc/resolv.conf', newData, function(err) {
if (err) throw err;
});
}
});
}
}process.on('SIGINT', onExit);
process.on('SIGTERM', onExit);
function onExit() {
var nameserverLine = 'nameserver ' + options.listenHost;
if (process.platform == 'darwin') {
log('removing /etc/resolver/dev');
fs.unlink('/etc/resolver/dev', function(err) {
if (err) throw err;
process.exit();
});
} else {
fs.readFile('/etc/resolv.conf', { encoding: 'utf8' }, function(err, data) {
if (err) throw err;
if (data.indexOf(nameserverLine) == 0) {
log('removing', nameserverLine, 'from /etc/resolv.conf');
var newData = data.replace(nameserverLine + '\n', '');
fs.writeFile('/etc/resolv.conf', newData, function(err) {
if (err) throw err;
process.exit();
});
} else process.exit();
});
}
}