fastcgi
Version:
Simple, robust node.js web server that runs FastCGI applications
489 lines (400 loc) • 15 kB
JavaScript
// FastCGI
//
// Copyright 2011 Iris Couch
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Specification: http://www.fastcgi.com/drupal/node/22
require('defaultable').def(module,
{ 'log': console
}, function(module, exports, DEFS) {
var net = require('net')
var URL = require('url')
var util = require('util')
var http = require('http')
var FCGI = require('fastcgi-parser')
var FastCGIStream = require('./stream')
var LOG = DEFS.log
module.exports = { 'httpd': httpd
, 'find_header_break': find_header_break
}
var RECORD_NAMES = learn_record_names()
// Connect to a FastCGI service and run an HTTP front-end sending all requests to it.
function httpd(port, host, socket_path, callback) {
connect_fcgi(socket_path, 0, function(er, socket) {
if(er)
return callback(er)
fcgi_get_values(socket, function(er, values) {
if(er)
return callback(er)
values.FCGI_MPXS_CONNS = values.FCGI_MPXS_CONNS || 0
LOG.info('FCGI values: %j', values)
var server = http.createServer(fcgi_handler(port, host, values, socket, socket_path))
server.listen(port, host)
return callback(null)
})
})
}
function fcgi_get_values(socket, callback) {
LOG.info('Get FastCGI values')
socket.on('data', on_data)
var values = [ ['FCGI_MAX_CONNS' , '']
, ['FCGI_MAX_REQS' , '']
, ['FCGI_MPXS_CONNS', '']
]
var writer = new FCGI.writer
writer.encoding = 'binary'
writer.writeHeader({ 'version' : FCGI.constants.version
, 'type' : FCGI.constants.record.FCGI_GET_VALUES
, 'recordId': 0
, 'contentLength': FCGI.getParamLength(values)
, 'paddingLength': 0
})
writer.writeParams(values)
socket.write(writer.tobuffer())
writer.writeHeader({ 'version' : FCGI.constants.version
, 'type' : FCGI.constants.record.FCGI_GET_VALUES
, 'recordId': 0
, 'contentLength': 0
, 'paddingLength': 0
})
socket.write(writer.tobuffer())
LOG.info('Listening for FastCGI values')
var fcgi_values = {}
var timeout = setTimeout(got_all_values, 100)
function on_data(data) {
var parser = new FCGI.parser
parser.encoding = 'utf8'
parser.onRecord = on_record
parser.onError = on_error
parser.execute(data)
}
function on_error(er) {
LOG.error('Error getting FastCGI values: %s', er.message || er)
parser.onRecord = parser.onError = function() {}
callback(er)
}
function on_record(record) {
var params = record.body.params || {}
, keys = Object.keys(params)
keys.forEach(function(key) {
fcgi_values[key] = num_or_str(params[key])
})
if(keys.length == 0)
got_all_values()
}
function got_all_values() {
clearTimeout(timeout)
socket.removeListener('data', on_data)
callback(null, fcgi_values)
}
}
function fcgi_handler(port, server_addr, features, socket, socket_path) {
var request_id = 0
, requests_in_flight = {}
, pending_requests = []
, fcgi_stream = null
prep_socket()
return on_request
function on_request(req, res) {
//LOG.info('Request: %j', req.url)
request_id += 1
var fcgi_request = { 'id': request_id
, 'req': req
, 'res':res
, 'stdout': []
, 'stderr': []
, 'keepalive': FCGI.constants.keepalive.OFF
}
pending_requests.push(fcgi_request)
process_request()
}
function process_request() {
if(!socket)
return //LOG.info('Postpone request until FastCGI is back up')
if(Object.keys(requests_in_flight).length && features.FCGI_MPXS_CONNS == 0)
return //LOG.info('Postpone request for non-multiplexed FastCGI')
var fcgi_request = pending_requests.shift()
if(!fcgi_request)
return //LOG.info('No requests to process')
else
requests_in_flight[fcgi_request.id] = fcgi_request
var req = fcgi_request.req
, res = fcgi_request.res
var req_url = URL.parse(req.url)
var cgi = { 'PATH_INFO': req_url.pathname
, 'SERVER_NAME': server_addr || 'unknown'
, 'SERVER_PORT': port
, 'SERVER_PROTOCOL': 'HTTP/1.1'
, 'SERVER_SOFTWARE': 'Node/' + process.version
}
Object.keys(req.headers).forEach(function(header) {
var key = 'HTTP_' + header.toUpperCase().replace(/-/g, '_')
cgi[key] = req.headers[header]
})
cgi.REQUEST_METHOD = req.method
cgi.QUERY_STRING = req_url.query || ''
if('content-length' in req.headers)
cgi.CONTENT_LENGTH = req.headers['content-length']
if('content-type' in req.headers)
cgi.CONTENT_TYPE = req.headers['content-type']
if('authorization' in req.headers)
cgi.AUTH_TYPE = req.headers.authorization.split(/ /)[0]
var params = Object.keys(cgi).map(function(key) { return [key, cgi[key]] })
params.forEach(function(param) {
//console.log(' Param: %s = %j', param[0], param[1])
})
// Write the request to FastCGI.
//LOG.info('Write request %d to FastCGI: %j', fcgi_request.id, req.url)
var writer = new FCGI.writer
writer.encoding = 'binary'
// Begin
writer.writeHeader({ 'version' : FCGI.constants.version
, 'type' : FCGI.constants.record.FCGI_BEGIN
, 'recordId': fcgi_request.id
, 'contentLength': 8
, 'paddingLength': 0
})
writer.writeBegin({ 'role': FCGI.constants.role.FCGI_RESPONDER
, 'flags': fcgi_request.keepalive
})
socket.write(writer.tobuffer())
// Parameters
writer.writeHeader({ 'version' : FCGI.constants.version
, 'type' : FCGI.constants.record.FCGI_PARAMS
, 'recordId': fcgi_request.id
, 'contentLength': FCGI.getParamLength(params)
, 'paddingLength': 0
})
writer.writeParams(params)
socket.write(writer.tobuffer())
// End parameters
writer.writeHeader({ 'version' : FCGI.constants.version
, 'type' : FCGI.constants.record.FCGI_PARAMS
, 'recordId': fcgi_request.id
, 'contentLength': 0
, 'paddingLength': 0
})
socket.write(writer.tobuffer())
// STDIN
if(req.method != 'PUT' && req.method != 'POST')
end_request()
else {
req.on('data', function(chunk) {
writer.writeHeader({ 'version' : FCGI.constants.version
, 'type' : FCGI.constants.record.FCGI_STDIN
, 'recordId': fcgi_request.id
, 'contentLength': chunk.length
, 'paddingLength': 0
})
writer.writeBody(chunk)
var data = writer.tobuffer()
socket.write(data)
})
req.on('end', end_request)
}
function end_request() {
writer.writeHeader({ 'version' : FCGI.constants.version
, 'type' : FCGI.constants.record.FCGI_STDIN
, 'recordId': fcgi_request.id
, 'contentLength': 0
, 'paddingLength': 0
})
socket.write(writer.tobuffer())
// At this point the request can be considered sent to the server, and it would be dangerous to re-send without knowing
// more details.
//console.log('Sent request %d: %s', fcgi_request.id, fcgi_request.req.url)
fcgi_request.sent = true
}
}
function prep_socket() {
fcgi_stream = new FastCGIStream
fcgi_stream.on('data', on_data)
fcgi_stream.on('end', on_end)
socket.pipe(fcgi_stream)
process_request()
}
function on_end() {
//LOG.info('FastCGI socket closed')
socket = null
var in_flight_ids = Object.keys(requests_in_flight)
, aborts = []
in_flight_ids.forEach(function(in_flight_id) {
var request_in_flight = requests_in_flight[in_flight_id]
delete requests_in_flight[in_flight_id]
if(request_in_flight.sent && request_in_flight.req.method != 'GET')
aborts.push(request_in_flight)
else {
// This can be retried when FastCGI comes back on-line.
if(request_in_flight.sent && request_in_flight.req.method == 'GET')
LOG.info('Schedule retry GET request %d', request_in_flight.id)
request_in_flight.sent = false
pending_requests.unshift(request_in_flight)
}
})
if(aborts.length) {
LOG.warn('FastCGI socket closed with %d in-flight requests sent', aborts.length)
aborts.forEach(function(aborted_request) {
LOG.warn(' Req %d: %s', aborted_request.id, aborted_request.req.url)
aborted_request.res.end()
})
}
connect_fcgi(socket_path, 0, function(er, new_socket) {
if(er)
throw er // TODO
//LOG.info('Reconnected: %s', socket_path)
socket = new_socket
prep_socket()
})
}
function on_data(data) {
var parser = new FCGI.parser
parser.bodies = []
parser.encoding = 'binary'
parser.onBody = on_body
parser.onRecord = on_record
parser.onError = on_error
parser.execute(data)
}
function on_error(er) {
LOG.error('Error from FastCGI parser: %s', er.message || er)
throw er // TODO
}
function on_body(data, start, end) {
data = data.slice(start, end)
this.bodies.push(data)
}
// Handle incoming responder records.
function on_record(record) {
var parser = this
//LOG.info('Record %s: %s', RECORD_NAMES[record.header.type], record.header.recordId)
record.bodies = parser.bodies
parser.bodies = []
record.body_utf8 = function() {
return this.bodies
.map(function(data) { return data.toString() })
.join('')
}
var req_id = record.header.recordId
if(req_id == 0)
return LOG.info('Ignoring management record: %j', record)
var request = requests_in_flight[req_id]
if(!request)
return LOG.error('Record for unknown request: %s\n%s', req_id, util.inspect(request))
if(record.header.type == FCGI.constants.record.FCGI_STDERR)
return LOG.error('Error: %s', record.body_utf8().trim())
else if(record.header.type == FCGI.constants.record.FCGI_STDOUT) {
request.stdout = request.stdout.concat(record.bodies)
return send_stdout(request)
}
else if(record.header.type == FCGI.constants.record.FCGI_END) {
request.res.end()
LOG.info('%s %s %d', request.req.method, request.req.url, request.status)
delete requests_in_flight[req_id]
if(request.keepalive == FCGI.constants.keepalive.ON)
process_request() // If there are more in the queue, get to them now.
else
socket.end()
}
else {
LOG.info('Unknown record: %j', record)
Object.keys(FCGI.constants.record).forEach(function(type) {
if(record.header.type == FCGI.constants.record[type])
LOG.info('Unknown record type: %s', type)
})
}
}
function send_stdout(request) {
if(!request.status) {
//LOG.log('Look for headers and status: %d', request.id)
var data_so_far = Buffer.concat(request.stdout)
, header_break = find_header_break(data_so_far)
//LOG.log(' %d bytes so far, break: %j', data_so_far.length, header_break)
if(!header_break)
return LOG.log(' No complete headers yet in stdout') // Still waiting for all headers to arrive.
// Headers have arrived. Convert them into a .writeHead() and only write subsequent data.
request.stdout = [ data_so_far.slice(header_break.end, data_so_far.length) ]
var headers_section = data_so_far.slice(0, header_break.start).toString('utf8')
, lines = headers_section.split(/\r?\n/)
, headers = {}
lines.forEach(function(line) {
var match = line.match(/^(.*?):\s(.*)$/)
, key = match && match[1].toLowerCase()
if(key == 'status')
request.status = parseInt(match[2]) || 200
else
headers[key] = match[2]
})
delete headers['accept-encoding']
request.res.writeHead(request.status, headers)
}
while(request.stdout.length > 0) {
var data = request.stdout.shift()
request.res.write(data)
}
}
}
function connect_fcgi(socket, attempts, callback) {
if(attempts > 5)
return callback(new Error('Failed to connect to back-end socket'))
// Try to connect to the back-end socket.
var fcgid = net.connect({'path':socket})
fcgid.on('error', on_error)
fcgid.on('connect', on_connect)
function on_connect() {
//LOG.info('Connected to FastCGI daemon: %s', socket)
fcgid.removeListener('error', on_error)
return callback(null, fcgid)
}
function on_error(er) {
if(er.code == 'ECONNREFUSED') {
var delay = 100 * Math.pow(2, attempts)
LOG.info('Waiting %d ms to connect', delay)
return setTimeout(function() { connect_fcgi(socket, attempts+1, callback) }, delay)
}
else if(er.code == 'ENOENT') {
LOG.error('Error: No such socket: %s', socket)
return callback(er)
}
else {
LOG.error('Unknown error on FastCGI connection: %s', er.message)
return callback(er)
}
}
}
//
// Utilities
//
function num_or_str(value) {
var num_value = +value
return isNaN(num_value) ? value : num_value
}
function learn_record_names() {
var types = {}
Object.keys(FCGI.constants.record).forEach(function(name) {
var value = FCGI.constants.record[name]
types[value] = name
})
return types
}
function find_header_break(data) {
var unix = new Buffer('\n\n')
, inet = new Buffer('\r\n\r\n')
for(var i = 0; i+2 <= data.length; i++) {
if(data[i] == unix[0] && data[i+1] == unix[1])
return {'start':i, 'end':i+2}
if(data[i] == inet[0] && data[i+1] == inet[1] && data[i+2] == inet[2] && data[i+3] == inet[3])
return {'start':i, 'end':i+4}
}
return null
}
}) // defaultable