fastcgi-parser
Version:
FastCGI parser for low level parsing of the FastCGI protocol
404 lines (380 loc) • 17 kB
JavaScript
/*
TODO:
* make the write handle a stream and write directly to it without creating buffers
* we assume everything is ascii
* allow chunked parsing of body (not really necessary as fastcgi protocol allows breaking up of body into separate messages
* allow writer to write to an existing buffer at offset passed in. should be a lot quicker than allocating and slicing...
* don't do pre-allocated buffers in parser. just pass back start and end of current buffer to the callee. should be a lot faster
* Add a resetonerror property to allow parser to stop processing immediately if an error is encountered
* Add a reset method to clear the current parser
* Pool of parsers?
* make buffering more efficent - maybe parse down into the body types and raise event for each param etc.
* think about overhead per connection
* allow a http stream to be parsed on the fly and wrapped in cgi (in and out). maybe inherit from node.js stream and implement pipe (like HTTPS)
* test for buffer overruns
* unit tests
* improve binary parser
*/
var constants = {
"version": 1,
"record": {
"FCGI_BEGIN": 1,
"FCGI_ABORT": 2,
"FCGI_END": 3,
"FCGI_PARAMS": 4,
"FCGI_STDIN": 5,
"FCGI_STDOUT": 6,
"FCGI_STDERR": 7,
"FCGI_DATA": 8,
"FCGI_GET_VALUES": 9,
"FCGI_GET_VALUES_RESULT": 10,
"FCGI_UNKNOWN_TYPE": 11
},
"keepalive": {
"OFF": 0,
"ON": 1
},
"parser": {
"state": {
"HEADER": 0,
"BODY": 1,
"PADDING": 2
}
},
"general": {
"FCGI_HEADER_LEN": 8,
"FCGI_MAX_BODY": Math.pow(2,16)
},
"errors": {
"BUFFER_OVERFLOW": {
"err": 1,
"description": "buffer overflow"
},
"MAX_BODY_EXCEEDED": {
"err": 2,
"description": "a body greater than maximum body size was read/written"
}
},
"flags": {
"FCGI_KEEP_CONN": 1
},
"role": {
"FCGI_RESPONDER": 1,
"FCGI_AUTHORIZER": 2,
"FCGI_FILTER": 3
},
"protocol": {
"status": {
"FCGI_REQUEST_COMPLETE": 0,
"FCGI_CANT_MPX_CONN": 1,
"FCGI_OVERLOADED": 2,
"FCGI_UNKNOWN_ROLE": 3
}
},
"values": {
"FCGI_MAX_CONNS": "FCGI_MAX_CONNS",
"FCGI_MAX_REQS": "FCGI_MAX_REQS",
"FCGI_MPXS_CONNS": "FCGI_MPXS_CONNS"
}
}
function Parser() {
var _parser = this;
var _loc = 0;
var _record = null;
// these are in here because code runs fastest with them in here
var FCGI_MAX_BODY = constants.general.FCGI_MAX_BODY;
var HEADER = constants.parser.state.HEADER;
var BODY = constants.parser.state.BODY;
var PADDING = constants.parser.state.PADDING;
var FCGI_HEADER_LEN = constants.general.FCGI_HEADER_LEN;
var FCGI_BEGIN = constants.record.FCGI_BEGIN;
var FCGI_ABORT = constants.record.FCGI_ABORT;
var FCGI_END = constants.record.FCGI_END;
var FCGI_PARAMS = constants.record.FCGI_PARAMS;
var FCGI_STDIN = constants.record.FCGI_STDIN;
var FCGI_STDOUT = constants.record.FCGI_STDOUT;
var FCGI_STDERR = constants.record.FCGI_STDERR;
var FCGI_DATA = constants.record.FCGI_DATA;
var FCGI_GET_VALUES = constants.record.FCGI_GET_VALUES;
var FCGI_GET_VALUES_RESULT = constants.record.FCGI_GET_VALUES_RESULT;
var FCGI_UNKNOWN_TYPE = constants.record.FCGI_UNKNOWN_TYPE;
var BUFFER_OVERRUN = constants.errors.BUFFER_OVERRUN;
var _header = new Buffer(FCGI_HEADER_LEN);
var _body = new Buffer(FCGI_MAX_BODY);
_parser.current = _record;
_parser.init = function() {
_parser.encoding = "utf8";
_parser.onRecord = _parser.onError = _parser.onHeader = _parser.onParam = _parser.onBody = function(){};
_parser.reset();
}
_parser.reset = function() {
_record = {
"header": {
"version": 0,
"type": 0,
"recordId": 0,
"contentLength": 0,
"paddingLength": 0
},
"body": {}
};
_parser.state = constants.parser.state.HEADER;
_loc = 0;
}
_parser.init();
_parser.execute = function(buffer, start, end) {
if(!start) start = 0;
if(!end) end = buffer.length;
for (var i = start; i < end; i++) {
switch(_parser.state) {
case HEADER:
if(_loc == FCGI_HEADER_LEN - 1) {
var header = _record.header;
_header[_loc] = buffer[i];
var j = 0;
header.version = _header[j++];
header.type = _header[j++];
header.recordId = (_header[j++] << 8) + _header[j++];
header.contentLength = (_header[j++] << 8) + _header[j++];
header.paddingLength = _header[j++];
_record.body = {};
if(_record.header.contentLength > 0) {
_parser.onHeader(header);
_parser.state = BODY;
}
else {
_parser.onRecord(_record);
}
_loc=0;
}
else {
_header[_loc++] = buffer[i];
}
break;
case BODY:
if(_loc == _record.header.contentLength - 1) {
_body[_loc] = buffer[i];
switch(_record.header.type) {
case FCGI_BEGIN:
var j = 0;
_record.body = {
"role": (_body[j++] << 8) + _body[j++],
"flags": _body[j++]
}
break;
case FCGI_ABORT:
break;
case FCGI_END:
var j = 0;
_record.body = {
"status": (_body[j++] << 24) + (_body[j++] << 16) + (_body[j++] << 8) + _body[j++],
"protocolStatus": _body[j++]
}
break;
case FCGI_PARAMS:
case FCGI_GET_VALUES:
case FCGI_GET_VALUES_RESULT:
var j = 0, name = "", value = "", vlen = 0, nlen = 0;
_record.body.params = {};
var rlen = _record.header.contentLength;
while(j < rlen) {
nlen = _body[j];
if(nlen >> 7 == 1) {
nlen = ((_body[j++] << 24) + (_body[j++] << 16) + (_body[j++] << 8) + _body[j++]) & 0x7fffffff;
}
else {
j++;
}
vlen = _body[j];
if(vlen >> 7 == 1) {
vlen = ((_body[j++] << 24) + (_body[j++] << 16) + (_body[j++] << 8) + _body[j++]) & 0x7fffffff;
}
else {
j++;
}
if((j + nlen + vlen) <= _body.length) {
var nv = _body.asciiSlice(j, j + nlen + vlen);
j += (nlen + vlen);
name = nv.substring(0, nlen);
value = nv.substring(nlen);
_parser.onParam(name, value);
_record.body.params[name] = value;
}
else {
_parser.onError(new Error(JSON.stringify(BUFFER_OVERRUN)));
j = rlen;
}
}
break;
case FCGI_STDIN:
case FCGI_STDOUT:
case FCGI_STDERR:
case FCGI_DATA:
switch(_parser.encoding) {
case "utf8":
_record.body = _body.utf8Slice(0, _record.header.contentLength);
break;
case "ascii":
_record.body = _body.asciiSlice(0, _record.header.contentLength);
break;
default:
_parser.onBody(_body, 0, _record.header.contentLength);
break;
}
break;
case FCGI_UNKNOWN_TYPE:
default:
_record.body = {
"type": _body[0]
}
break;
}
_parser.onRecord(_record);
_loc = 0;
if(_record.header.paddingLength > 0) {
_parser.state = PADDING;
}
else {
_parser.state = HEADER;
}
}
else {
_body[_loc++] = buffer[i];
}
break;
case PADDING:
if(_loc++ == _record.header.paddingLength - 1) {
_parser.state = HEADER;
_loc = 0;
}
break;
}
}
}
}
//TODO: add buffer overrun checks - won't be get errors thrown from pack method?? if so, add try/catch blocks
function Writer() {
var _writer = this;
var _pos = 0;
var FCGI_HEADER_LEN = constants.general.FCGI_HEADER_LEN;
var FCGI_MAX_BODY = constants.general.FCGI_MAX_BODY;
var _buffer = new Buffer(FCGI_MAX_BODY + FCGI_HEADER_LEN);
_writer.encoding = "utf8";
_writer.buffer = {
"length": 0,
"buffer": _buffer
};
_writer.tobuffer = function() {
return _buffer.slice(0, _writer.buffer.length);
}
_writer.writeHeader = function(header) {
var i = 0;
_buffer[i++] = header.version & 0xff;
_buffer[i++] = header.type & 0xff;
_buffer[i++] = (header.recordId >> 8) & 0xff;
_buffer[i++] = header.recordId & 0xff;
_buffer[i++] = (header.contentLength >> 8) & 0xff;
_buffer[i++] = header.contentLength & 0xff;
_buffer[i++] = header.paddingLength & 0xff;
_buffer[i++] = 0;
_writer.buffer.length = header.contentLength + header.paddingLength + FCGI_HEADER_LEN;
_pos = i;
}
_writer.writeParams = function(params) {
var i = _pos;
var plen = params.length;
var index = 0;
// loop optimisation
while(plen--) {
var param = params[index++];
var name = param[0];
var value = param[1].toString();
var nlen = name.length;
var vlen = value.length;
if(nlen > 127) {
var nlen1 = nlen | 0x80000000;
_buffer[i++] = (nlen1 >> 24) & 0xff;
_buffer[i++] = (nlen1 >> 16) & 0xff;
_buffer[i++] = (nlen1 >> 8) & 0xff;
_buffer[i++] = nlen1 & 0xff;
}
else {
_buffer[i++] = nlen & 0xff;
}
if(vlen > 127) {
var vlen1 = vlen | 0x80000000;
_buffer[i++] = (vlen1 >> 24) & 0xff;
_buffer[i++] = (vlen1 >> 16) & 0xff;
_buffer[i++] = (vlen1 >> 8) & 0xff;
_buffer[i++] = vlen1 & 0xff;
}
else {
_buffer[i++] = vlen & 0xff;
}
//_buffer.write(name + value, i, "ascii");
_buffer.asciiWrite(name + value, i, nlen + vlen);
i += (nlen + vlen);
}
_pos = i;
}
_writer.writeBody = function(body) {
switch(_writer.encoding) {
case "ascii":
_pos += _buffer.asciiWrite(body, _pos, _buffer.length - _pos);
break;
case "utf8":
_pos += _buffer.utf8Write(body, _pos, _buffer.length - _pos);
break;
default:
body.copy(_buffer, _pos);
_pos += body.length;
break;
}
}
_writer.writeBegin = function(begin) {
var i = _pos;
_buffer[i++] = (begin.role >> 8) & 0xff;
_buffer[i++] = begin.role & 0xff;
_buffer[i++] = begin.flags & 0xff;
_buffer[i++] = 0;
_buffer[i++] = 0;
_buffer[i++] = 0;
_buffer[i++] = 0;
_buffer[i++] = 0;
_pos = i;
}
_writer.writeEnd = function(end) {
var i = _pos;
_buffer[i++] = (end.status >> 24) & 0xff;
_buffer[i++] = (end.status >> 16) & 0xff;
_buffer[i++] = (end.status >> 8) & 0xff;
_buffer[i++] = end.status & 0xff;
_buffer[i++] = end.protocolStatus & 0xff;
_buffer[i++] = 0;
_buffer[i++] = 0;
_buffer[i++] = 0;
_pos = i;
}
}
exports.parser = Parser;
exports.writer = Writer;
exports.constants = constants;
exports.getParamLength = function(params) {
var size = 0;
params.forEach(function(param) {
size += (param[0].length + param[1].toString().length);
if(param[0].length > 127) {
size += 4;
}
else {
size++;
}
if(param[1].toString().length > 127) {
size += 4;
}
else {
size++;
}
});
return size;
}