index.js
'use strict';
var fcgi = require('fastcgi-stream');
var http = require('http');
var net = require('net');
var stream = require('stream');
// FCGI record content must not exceed 65535 bytes of data.
var FCGI_MAX_CONTENT_LEN = 65535;
/**
* @param appCb function to be called with `http.ClientRequest` and `http.ServerResponse` objects
* @param errCb socket error handler
* @returns {connectFunction}
*/
function createHandler(appCb, errCb) {
errCb = errCb || function (e) {
console.error(e);
console.error(e.stack);
};
return function connectFunction(socket) {
// Register an error handler so the process doesn't get killed in case of socket read errors, etc.
socket.on('error', errCb);
var fastcgiStream = new fcgi.FastCGIStream(socket);
var requests = {};
fastcgiStream.on('record', function (requestId, record) {
var request = requests[requestId];
if (record instanceof fcgi.records.BeginRequest) {
if (request) {
socket.end();
}
var incomingStream = new stream.PassThrough();
request = requests[requestId] = {
req: new http.IncomingMessage(incomingStream)
};
// Express relies on this property being non-null.
request.req.connection = {};
// FastCGI environment variables.
request.req.env = {};
} else if (record instanceof fcgi.records.Params) {
record.params.forEach(function (paramPair) {
var key = paramPair[0].toLowerCase().replace(/_/g, '-');
var val = paramPair[1];
if (key && key.length && val && val.length) {
if (key.indexOf('http-') === 0) {
// This is a browser header.
key = key.substr(5);
request.req._addHeaderLine(key, val, request.req.headers);
} else {
// This is an FCGI environment property.
switch (key) {
case 'server-protocol':
var httpVersionParts = val.replace(/^HTTP\//, '').split('.');
if (httpVersionParts.length != 2)
httpVersionParts = [1, 1];
request.req.httpVersionMajor = Number(httpVersionParts[0]);
request.req.httpVersionMinor = Number(httpVersionParts[1]);
request.req.httpVersion = request.req.httpVersionMajor + '.' + request.req.httpVersionMinor;
break;
case 'request-uri':
request.req.url = val;
break;
case 'request-method':
request.req.method = val;
break;
case 'request-scheme':
request.req.connection.encrypted = (val === 'https');
break;
case 'remote-addr':
request.req.connection.remoteAddress = val;
break;
case 'content-length':
case 'content-type':
// These are CGI headers that need to be present in the HTTP request.
// No break here - we want these to be present on the `env` property too.
request.req._addHeaderLine(key, val, request.req.headers);
default:
// Make these available on a special `env` property on the request object.
request.req.env[key] = val;
break;
}
}
}
});
if (record.params.length == 0) {
// Setup http response.
request.res = new http.ServerResponse(request.req);
var writable = new stream.Writable();
writable._write = function (chunk, encoding, next) {
if (!Buffer.isBuffer(chunk)) {
chunk = new Buffer(chunk, encoding);
encoding = buffer;
}
var currChunk;
var record;
var consumed = 0;
var remaining = chunk.length;
do {
currChunk = chunk.slice(consumed, consumed + Math.min(remaining, FCGI_MAX_CONTENT_LEN));
record = new fcgi.records.StdOut(currChunk);
record.encoding = encoding;
fastcgiStream.writeRecord(requestId, record);
consumed += FCGI_MAX_CONTENT_LEN;
remaining -= FCGI_MAX_CONTENT_LEN;
} while (remaining > 0);
next();
};
// When we try to write more data to the 'writable' stream than the highWaterMark,
// it will pause the readable stream, so we need to pass on the 'drain' notification.
writable.on('drain', function () {
request.res.emit('drain');
});
request.res.assignSocket(writable);
// TODO: would be nice to support this, but it's causing weird
// shit when sent over the FCGI wire.
request.res.useChunkedEncodingByDefault = false;
// Sorta hacky, we override the _storeHeader implementation of
// OutgoingMessage and blank out the http response header line.
// Instead, we parse it out and put it into the Status http header.
// TODO: should we check if we're supposed to be sending NPH or
// something? Can we even do that in FCGI?
request.res._storeHeader = function (statusLine, headers) {
var matches = statusLine.match(/^HTTP\/[0-9]\.[0-9] (.+)/);
if (!headers) {
headers = {};
}
headers['Status'] = matches[1];
http.OutgoingMessage.prototype._storeHeader.apply(this, ['', headers]);
};
request.res.on('finish', function () {
var end = new fcgi.records.EndRequest(0, fcgi.records.EndRequest.protocolStatus.REQUEST_COMPLETE);
fastcgiStream.writeRecord(requestId, end);
socket.end();
});
try {
appCb(request.req, request.res);
} catch (e) {
var end = new fcgi.records.EndRequest(-1, fcgi.records.EndRequest.protocolStatus.REQUEST_COMPLETE);
fastcgiStream.writeRecord(requestId, end);
socket.end();
throw e;
}
}
} else if (record instanceof fcgi.records.StdIn) {
if (record.data.length) {
request.req.emit('data', record.data);
} else {
// Emit 'end' on the IncomingMessage.
request.req.emit('end');
}
}
});
};
};
exports.createServer = function(app) {
return net.createServer(createHandler(app));
};
exports.listenFd = function(app, fd) {
var server = exports.createServer(app);
server.listen({fd: fd});
return server;
};
exports.listenStdin = function(app) {
return exports.listenFd(app, process.stdin.fd);
};
exports.listenPort = function(app, port) {
var server = exports.createServer(app);
server.listen(port);
return server;
};