UNPKG

connect-yii

Version:

connect middleware to pass requests to FastCGI server

471 lines (444 loc) 13.1 kB
var net = require("net"); var fastcgi = require("fastcgi-parser"); var dns = require("dns"); var events = require("events"); var url = require("url"); var inherits = require("util").inherits; var HTTPParser = process.binding("http_parser").HTTPParser; var stream = require("stream"); /* * support X-SendFile */ var gid = 0; function client(options) { var _fastcgi = this; var connection = new net.Stream(); var htparser = new HTTPParser("response"); var queue = []; var reqid = 0; var requests = {}; var stats = { connections: 0 } _fastcgi.stats = stats; var phprx = new RegExp("Status: (\\d{3}) (.*?)\\r\\n"); var port = options.port || "/tmp/php.sock"; var host = options.host; var keepalive = options.keepAlive || false; var shost = options.server.host || "127.0.0.1"; var sport = options.server.port || 80; var sname = options.server.name || "localhost"; var sredirectstatus = options.redirectStatus || null; var _current = null; ////console.log(" --> psname",psname); connection.setNoDelay(true); connection.setTimeout(0); connection.params = [ ["DOCUMENT_ROOT", options.root || "/var/www/html"], ["SERVER_PROTOCOL", "HTTP/1.1"], ["GATEWAY_INTERFACE", "CGI/1.1"], ["SERVER_SOFTWARE", "node.js"], ["SERVER_ADDR", shost.toString()], ["SERVER_PORT", sport.toString()], ["SERVER_NAME", sname] ]; connection.writer = new fastcgi.writer(); connection.parser = new fastcgi.parser(); connection.parser.encoding = "binary"; connection.writer.encoding = "binary"; htparser.onHeaderField = function (b, start, len) { var slice = b.toString('ascii', start, start+len).toLowerCase(); if (htparser.value != undefined) { var dest = _current.fcgi.headers; if (htparser.field in dest) { dest[htparser.field].push(htparser.value); } else { dest[htparser.field] = [htparser.value]; } htparser.field = ""; htparser.value = ""; } if (htparser.field) { htparser.field += slice; } else { htparser.field = slice; } }; htparser.onHeaderValue = function (b, start, len) { //console.log(" -> onHeaderValue",b.toString('ascii', start, start+len)); var slice = b.toString('ascii', start, start+len); if (htparser.value) { htparser.value += slice; } else { htparser.value = slice; } }; htparser.onHeadersComplete = function (info) { //console.log(" -> info:",info); if (htparser.field && (htparser.value != undefined)) { var dest = _current.fcgi.headers; if (htparser.field in dest) { dest[htparser.field].push(htparser.value); } else { dest[htparser.field] = [htparser.value]; } htparser.field = null; htparser.value = null; } _current.fcgi.info = info; _current.resp.statusCode = info.statusCode; for(header in _current.fcgi.headers) { var head = _current.fcgi.headers[header]; if(head.length > 1) { _current.resp.setHeader(header, head); } else { _current.resp.setHeader(header, head[0]); } } } htparser.onBody = function(buffer, start, len) { //console.log(" -> onBody"); _current.resp.write(buffer.slice(start, start + len)); } connection.parser.onHeader = function(header) { //console.log(" -> Header",header); _current = requests[header.recordId]; } connection.parser.onBody = function(buffer, start, len) { //console.log(" -> connection.parser.onBody"); //console.log(buffer.toString("utf8", start, start + len)); if(!_current.fcgi.body) { htparser.reinitialize("response"); _current.fcgi.headers = {}; var content = buffer.toString("binary",start, start + len); var contents = content.split("\r\n\r\n"); var status = contents[0]; var match = status.match(phprx); // Add headers if(typeof contents[1] != "undefined") { // We have headers to use. var hdrs = contents[0].split("\r\n"); for(i=0; i<hdrs.length; i++) { var parts = hdrs[i].split(": "); _current.resp.setHeader(parts[0], parts[1]); } } if(match) { var header = match[0]; var buff = buffer.slice(start + header.length, start + len); status = new Buffer("HTTP/1.1 " + match[1] + " " + match[2] + "\r\n"); try { var parsed = htparser.execute(status, 0, status.length); var parsed = htparser.execute(buffer, start, len); if(parsed.bytesParsed) { _current.resp.write(buff.slice(start + parsed.bytesParsed, start + len)); } _current.resp.statusCode = match[1]; } catch(ex) { _current.cb(ex); } } else { status = new Buffer("HTTP/1.1 200 OK\r\n"); try { var parsed = htparser.execute(status, 0, status.length); var parsed = htparser.execute(buffer, start, len); if(parsed.bytesParsed) { _current.resp.write(buffer.slice(start + parsed.bytesParsed, start + len)); } _current.resp.statusCode = 200; } catch(ex) { _current.cb(ex); } } var toPrint;// = contents[1] || contents[0]; if(typeof contents[1] == "undefined") { toPrint = contents[0]; } else { toPrint = contents[1]; } ////console.log(" -> Printing:", toPrint); ////console.log(" -> Contents:",contents); _current.resp.write(toPrint); _current.fcgi.body = true; } else { try { var parsed = htparser.execute(buffer, start, len); ////console.log("-> Parsed message:",parsed); if(parsed.message == "Parse Error" && ("bytesParsed" in parsed)) { _current.resp.write(buffer.slice(start + parsed.bytesParsed, start + len)); } } catch(ex) { _current.cb(ex); } } } connection.parser.onRecord = function(record) { //console.log(" -> onRecord"); var recordId = parseInt(record.header.recordId); var request = requests[recordId]; switch(record.header.type) { case fastcgi.constants.record.FCGI_END: request.fcgi.status = record.body; if(record.body.status == 0 && record.body.protocolStatus == 0) { request.resp.end(); delete requests[recordId]; request.cb(null, record); } else { // error from fastcgi app - return 500; // if we want to use this (php doesn't) we need to buffer the whole response and wait to inspect this before sending any headers. that's just nasty!! } if(keepalive && queue.length > 0){ next(); } break; default: break; } }; connection.parser.onError = function(err) { //console.log("onError", err); _fastcgi.emit("error", err); }; if(sredirectstatus) { connection.params.push(["REDIRECT_STATUS", sredirectstatus.toString()]); } connection.ondata = function (buffer, start, end) { //console.log(" -> ondata"); connection.parser.execute(buffer, start, end); }; connection.addListener("connect", function() { stats.connections++; if(queue.length > 0) { next(); } }); connection.addListener("timeout", function() { connection.end(); }); connection.addListener("end", function() { if(queue.length > 0) { process.nextTick(_fastcgi.connect); } }); connection.addListener("error", function(err) { _fastcgi.emit("error", err); connection.end(); }); this.connect = function() { if(!connection.fd) connection.connect(port, host); } this.end = function() { connection.end(); } function next() { //debugger; var request = queue.shift(); var req = request.req; req.resume(); var params = connection.params.slice(0); params = req.php_headers; //console.log("Request headers", req.headers); //TODO: probably better to find a generic way of translating all http headers on request into PHP headers if("user-agent" in req.headers) { params.push(["HTTP_USER_AGENT", req.headers["user-agent"]]); } if("accept-encoding" in req.headers) { params.push(["HTTP_ACCEPT_ENCODING", req.headers["accept-encoding"]]); } if("cookie" in req.headers) { params.push(["HTTP_COOKIE", req.headers["cookie"]]); } if("connection" in req.headers) { params.push(["HTTP_CONNECTION", req.headers["connection"]]); } if("accept" in req.headers) { params.push(["HTTP_ACCEPT", req.headers["accept"]]); } if("host" in req.headers) { params.push(["HTTP_HOST", req.headers["host"]]); } if("content-type" in req.headers) { params.push(["CONTENT_TYPE", req.headers["content-type"]]); } if("content-length" in req.headers) { params.push(["CONTENT_LENGTH", req.headers["content-length"]]); } try { connection.writer.writeHeader({ "version": fastcgi.constants.version, "type": fastcgi.constants.record.FCGI_BEGIN, "recordId": request.id, "contentLength": 8, "paddingLength": 0 }); connection.writer.writeBegin({ "role": fastcgi.constants.role.FCGI_RESPONDER, "flags": keepalive?fastcgi.constants.keepalive.ON:fastcgi.constants.keepalive.OFF }); connection.write(connection.writer.tobuffer()); connection.writer.writeHeader({ "version": fastcgi.constants.version, "type": fastcgi.constants.record.FCGI_PARAMS, "recordId": request.id, "contentLength": fastcgi.getParamLength(params), "paddingLength": 0 }); connection.writer.writeParams(params); connection.write(connection.writer.tobuffer()); connection.writer.writeHeader({ "version": fastcgi.constants.version, "type": fastcgi.constants.record.FCGI_PARAMS, "recordId": request.id, "contentLength": 0, "paddingLength": 0 }); connection.write(connection.writer.tobuffer()); switch(req.method) { case "GET": connection.writer.writeHeader({ "version": fastcgi.constants.version, "type": fastcgi.constants.record.FCGI_STDIN, "recordId": request.id, "contentLength": 0, "paddingLength": 0 }); connection.write(connection.writer.tobuffer()); break; case "PUT": request.cb(new Error("PUT: not implemented")); break; case "POST": if(typeof req.rawBody == "undefined") { req.on("data", function(chunk) { //console.log(" -> POST:",chunk); connection.writer.writeHeader({ "version": fastcgi.constants.version, "type": fastcgi.constants.record.FCGI_STDIN, "recordId": request.id, "contentLength": chunk.length, "paddingLength": 0 }); connection.writer.writeBody(chunk); connection.write(connection.writer.tobuffer()); }); req.on("end", function() { connection.writer.writeHeader({ "version": fastcgi.constants.version, "type": fastcgi.constants.record.FCGI_STDIN, "recordId": request.id, "contentLength": 0, "paddingLength": 0 }); connection.write(connection.writer.tobuffer()); }); } else { // A raw body exists, so we can use it. var rbBuf = req.rawBody; var size = fastcgi.constants.general.FCGI_MAX_BODY-1; var st = new stream.Readable; st.__i = 0; st._read = function(n) { if(this.__i*size >= rbBuf.length+1) { return this.push(null); } var from = (this.__i*size); var to = ((this.__i*size)+size); var nbuf = rbBuf.slice(from, to); this.push(nbuf); console.log("Stats",{ i: this.__i, from: from, to: to, length: rbBuf.length, "content-length": req.headers["content-length"], "buffer-length": nbuf.length }); this.__i++; } st.on("data", function(chunk) { connection.writer.writeHeader({ "version": fastcgi.constants.version, "type": fastcgi.constants.record.FCGI_STDIN, "recordId": request.id, "contentLength": chunk.length, "paddingLength": 0 }); connection.writer.writeBody(chunk); connection.write(connection.writer.tobuffer()); }); st.on("end", function() { connection.writer.writeHeader({ "version": fastcgi.constants.version, "type": fastcgi.constants.record.FCGI_STDIN, "recordId": request.id, "contentLength": 0, "paddingLength": 0 }); connection.write(connection.writer.tobuffer()); }); } break; case "DELETE": request.cb(new Error("DELETE: not implemented")); break; } } catch(ex) { connection.end(); request.cb(ex); } } this.request = function(req, resp, cb) { requests[reqid] = { "id": reqid, "req": req, "resp": resp, "cb": cb, "fcgi": {} }; queue.push(requests[reqid]); reqid++; if(reqid == 65535) { reqid = 0; } if(!connection.fd) { req.pause(); _fastcgi.connect(); } else if(keepalive && queue.length == 1){ next(); } } } inherits(client, events.EventEmitter); function agent(nc, options) { var _agent = this; var i = nc; var current = 0; var clients = []; this.clients = clients; while(i--) { var c = new client(options); c.on("error", function(err) { _agent.emit("error", err); }); clients.push(c); } this.request = function(req, resp, cb) { var client = clients[current]; client.request(req, resp, cb); if(current == nc - 1) { current = 0; } else { current++; } } } inherits(agent, events.EventEmitter); exports.Client = client; exports.Agent = agent;