node-fastcgi
Version:
Create FastCGI applications in node. Near drop-in replacement for node's http module.
365 lines (295 loc) • 11.7 kB
JavaScript
/**
* Copyright (c) 2016 Fabio Massaioli and other contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
;
var net = require('net'),
util = require('util'),
fcgi = require('fastcgi-stream'),
Request = require('./request.js').Request;
exports.Server = Server;
var VALUE_MAP = {
'FCGI_MAX_CONNS': 'maxConns',
'FCGI_MAX_REQS': 'maxReqs',
'FCGI_MPXS_CONNS': 'multiplex'
};
function isFunction(obj) {
return typeof obj === 'function';
}
/**
* function Server([responder], [authorizer], [filter], [config])
* FastCGI server object. Compatible with http.Server
*
* Arguments:
* - responder (optional): callback for FastCGI responder requests (normal HTTP requests, 'request' event)
* - authorizer (optional): callback for FastCGI authorizer requests ('authorize' event)
* - filter (optional): callback for FastCGI filter requests ('filter' event)
* - config (optional): server configuration (default: { maxConns: 2000, maxReqs: 2000, multiplex: true, valueMap: {} })
*/
function Server(responder, authorizer, filter, config) {
if (!(this instanceof Server)) {
return new Server(responder, authorizer, filter, config);
}
net.Server.call(this, { allowHalfOpen: false });
config = config ||
(isFunction(filter) ? {} : filter ||
(isFunction(authorizer) ? {} : authorizer ||
(isFunction(responder) ? {} : responder || {})));
config.maxConns = parseInt(config.maxConns) || 2000;
config.maxReqs = parseInt(config.maxReqs) || 2000;
if (config.multiplex === undefined) {
config.multiplex = true;
} else {
config.multiplex = config.multiplex ? true : false;
}
config.valueMap = config.valueMap || {};
for (var v in VALUE_MAP) {
config.valueMap[v] = VALUE_MAP[v];
}
this.config = config;
this.maxHeadersCount = 0;
this.timeout = 2 * 60 * 1000;
if (isFunction(responder)) {
this.on('request', responder);
}
if (isFunction(authorizer)) {
this.on('authorize', authorizer);
}
if (isFunction(filter)) {
this.on('filter', filter);
}
this.on('connection', connectionListener);
}
util.inherits(Server, net.Server);
/**
* server.listen = function([options...], [callback])
* Starts listening on default FastCGI socket.
* Forwards to net.Server.listen if called with options.
*
* Arguments:
* - options (optional): net.Server.listen options
* - callback (optional): this parameter will be added as a listener
* for the 'listening' event.
*/
Server.prototype.listen = function (callback) {
if (arguments.length > 0 && !isFunction(callback)) {
return net.Server.prototype.listen.apply(this, arguments);
}
return net.Server.prototype.listen.call(this, process.stdin, callback);
};
Server.prototype.setTimeout = function (msecs, callback) {
this.timeout = msecs;
if (callback) {
this.on('timeout', callback);
}
};
function connectionListener(socket) {
var stream = new fcgi.FastCGIStream(socket),
requests = {};
function endRequest(id, status, reason, callback) {
if (reason === undefined) {
reason = fcgi.records.EndRequest.protocolStatus.REQUEST_COMPLETE;
}
if (socket.writable && !socket.destroyed) {
var closeConn = (id in requests && !requests[id]._keepAlive);
delete requests[id];
stream.writeRecord(
id, new fcgi.records.EndRequest(status, reason),
function (err) {
if (closeConn && !socket.destroyed) {
socket.end();
}
if (callback)
callback(err);
});
}
delete requests[id];
}
function abortAll(hadError) {
for (var id in requests) {
requests[id]._abort(hadError);
}
}
socket.stream = stream;
socket.endRequest = endRequest;
var self = this;
if (this.timeout) {
socket.setTimeout(this.timeout);
}
socket.on('timeout', function () {
var reqSocketTimeout = false, reqTimeout = false,
resTimeout = false, serverTimeout = false;
for (var id in requests) {
var request = requests[id];
reqSocketTimeout = reqSocketTimeout || request.emit('timeout', socket);
reqTimeout = reqTimeout || (request._req && request._req.emit('timeout', request));
resTimeout = resTimeout || (request._res && request._res.emit('timeout', request));
}
serverTimeout = self.emit('timeout', socket);
if (!reqSocketTimeout && !reqTimeout && !resTimeout && !serverTimeout) {
socket.destroy();
}
});
socket.once('error', function (err) {
socket.on('error', function () {});
if (!self.emit('clientError', err, socket)) {
socket.destroy();
}
});
socket.on('close', abortAll);
stream.on('record', function (id, record) {
if (record.TYPE == fcgi.records.GetValues.TYPE) {
var result = [];
record.values.forEach(function (v) {
if (v === 'FCGI_MPXS_CONNS') {
result.push([v, self.config.multiplex ? "1" : "0"]);
} else if (v in self.config.valueMap) {
result.push([v, self.config[self.config.valueMap[v]].toString()]);
}
});
stream.writeRecord(id, new fcgi.records.GetValuesResult(result));
} else if (record.TYPE == fcgi.records.BeginRequest.TYPE) {
if (id in requests) {
// Request id already in use
self.emit('protocolError', new Error("Request id " + id + " already in use"));
return;
}
if (!self.config.multiplex && requests.length > 0) {
endRequest(id, 1, fcgi.records.EndRequest.protocolStatus.CANT_MPX_CONN);
return;
}
var keepAlive = record.flags & fcgi.records.BeginRequest.flags.KEEP_CONN;
switch (record.role) {
case fcgi.records.BeginRequest.roles.RESPONDER:
if (self.listeners('request').length < 1) {
endRequest(id, 1, fcgi.records.EndRequest.protocolStatus.UNKNOWN_ROLE, function () {
if (!keepAlive && !socket.destroyed) {
socket.end();
}
});
return;
}
break;
case fcgi.records.BeginRequest.roles.AUTHORIZER:
if (self.listeners('authorize').length < 1) {
endRequest(id, 1, fcgi.records.EndRequest.protocolStatus.UNKNOWN_ROLE, function () {
if (!keepAlive && !socket.destroyed) {
socket.end();
}
});
return;
}
break;
case fcgi.records.BeginRequest.roles.FILTER:
if (self.listeners('filter').length < 1) {
endRequest(id, 1, fcgi.records.EndRequest.protocolStatus.UNKNOWN_ROLE, function () {
if (!keepAlive && !socket.destroyed) {
socket.end();
}
});
return;
}
break;
default:
endRequest(id, 1, fcgi.records.EndRequest.protocolStatus.UNKNOWN_ROLE, function () {
if (!keepAlive && !socket.destroyed) {
socket.end();
}
});
return;
}
requests[id] = new Request(socket, id, record.role, keepAlive);
} else {
if (!(id in requests)) {
// Invalid request id, ignore record
self.emit('protocolError', new Error("Request id " + id + " is invalid, ignoring record"));
return;
}
return recordHandler.call(self, socket, requests[id], record);
}
});
}
function recordHandler(conn, req, record) {
switch (record.TYPE) {
case fcgi.records.AbortRequest.TYPE:
req._abort();
return;
case fcgi.records.Params.TYPE:
record.params.forEach(function (p) {
p.length !== p.toString().length ? req._param(p[0], p[1]) : req._param(p.toString(), "");
});
if (record.params.length === 0) {
switch (req._role) {
case fcgi.records.BeginRequest.roles.RESPONDER:
req.dataStream._data(null);
req._dataComplete = true;
var reqRes = req._createReqRes();
this.emit('request', reqRes.req, reqRes.res);
break;
case fcgi.records.BeginRequest.roles.AUTHORIZER:
req.dataStream._data(null);
req._dataComplete = true;
var reqRes = req._createReqRes();
this.emit('authorize', reqRes.req, reqRes.res);
break;
case fcgi.records.BeginRequest.roles.FILTER:
var reqRes = req._createReqRes();
this.emit('filter', reqRes.req, reqRes.res);
break;
}
}
return;
case fcgi.records.StdIn.TYPE:
if (req._stdinComplete) {
return;
}
if (record.data.length > 0) {
req._data(record.data);
} else {
req._data(null);
req._stdinComplete = true;
if (req._req) {
req._req.complete = req._stdinComplete && req._dataComplete;
// NOTE: Backward compatibility
if (req._req.complete) {
req._req.emit('complete');
}
}
}
return;
case fcgi.records.Data.TYPE:
if (req._dataComplete) {
return;
}
if (record.data.length > 0) {
req.dataStream._data(record.data);
} else {
req.dataStream._data(null);
req._dataComplete = true;
if (req._req) {
req._req.complete = req._stdinComplete && req._dataComplete;
// NOTE: Backward compatibility
if (req._req.complete) {
req._req.emit('complete');
}
}
}
return;
}
}