Skip to content

Commit

Permalink
Runner for CGI scripts
Browse files Browse the repository at this point in the history
  • Loading branch information
piscisaureus committed Aug 6, 2010
1 parent 651e5b4 commit 77addfc
Show file tree
Hide file tree
Showing 2 changed files with 160 additions and 0 deletions.
155 changes: 155 additions & 0 deletions lib/antinode.js
Expand Up @@ -6,6 +6,7 @@ var http = require('http'),
log = require('./log'),
package = JSON.parse(fs.readFileSync(__dirname+'/../package.json', 'utf8')),
sys = require('sys'),
spawn = require('child_process').spawn,
Script = process.binding('evals').Script;

exports.default_settings = {
Expand Down Expand Up @@ -181,6 +182,11 @@ function map_request_to_local_file(req, resp) {
execute_sjs(req, resp, pathInfo);
break;

// run CGI handler
case "cgi":
execute_cgi(req, resp, pathInfo, runner);
break;

// fail!
default:
throw "Invalid runner type: " + sys.inspect(runner);
Expand Down Expand Up @@ -328,6 +334,155 @@ function execute_sjs(req, resp, pathInfo) {
});
}

// HTTP headers that shouldn't be set as HTTP_something in the environment
var specialHttpHeaders = /^(athorization|proxy-authorization|content-type|content-length)$/i;

// Regular expressions used for CGI response header parsing
var statusHeaderMatch = /^Status:\s*(\d+)\s*(\S.*)?$/i,
normalHeaderMatch = /^([^:]*)\:\s*(\S.*)?$/;

function execute_cgi(req, resp, pathInfo, runner) {
var reqHeaders = req.headers;
var parsedUrl = uri.parse(req.url);

// Split script path into directory and name
var cwd = pathlib.dirname(pathInfo.fileLocal);
var script = pathlib.basename(pathInfo.fileLocal);

// Add non-http CGI fields to env
var env = {
// Gateway and webserver info
SERVER_SOFTWARE : serverInfo,
GATEWAY_INTERFACE : 'CGI/1.1',

// Request envelope
SERVER_NAME : req.headers.host || '',
SERVER_PORT : settings.port,
SERVER_PROTOCOL : 'HTTP/' + req.httpVersion,
REQUEST_METHOD : req.method,

// Script and parameters
SCRIPT_NAME : pathInfo.file,
QUERY_STRING : parsedUrl.query || '',
PATH_INFO : pathInfo.extra || "",
PATH_TRANSLATED : pathInfo.extraLocal || "",

// Request body
CONTENT_TYPE : reqHeaders['content-type'] || '',
CONTENT_LENGTH : reqHeaders['content-length'] || '',

// Remote end
REMOTE_ADDR : req.connection.remoteAddress,
REMOTE_HOST : '', // not mandatory and we don't want to do a reverse DNS lookup
AUTH_TYPE : '', // unsupported (HTTP athentication related)
REMOTE_IDENT : '', // unsupported (HTTP athentication related)

// Non-standard; required to keep PHP-CGI happy
REDIRECT_STATUS : 'CGI',
SCRIPT_FILENAME : script
};

// Add other http header fields prefixed with `HTTP_`,
// except for content-type, content-length and authentication-related fields
for (var name in reqHeaders) {
if (reqHeaders.hasOwnProperty(name) && !specialHttpHeaders.test(name)) {
env["HTTP_" + name.replace('-', "_").toUpperCase()] = reqHeaders[name];
}
}

// Here we keep track of headers parsed so far
var statusCode = 200,
statusText = "OK",
respHeaders = {};

// Buffer unprocessed header text here when a data chunk ends with a partial header line
var bufferedHeaderText = "";

// Spawn cgi process
log.debug(cwd + "$ " + runner.exec + " " + script);
var child = spawn(runner.exec, [script], {env: env, cwd: cwd});

// We don't need this any more, free up memory
delete env;

// Pump request body straight to child process' stdin
sys.pump(req, child.stdin);

// Send child process' stderr to log so we know when something's going wrong
child.stderr.setEncoding('utf8');
child.stderr.on('data', function(chunk) {
log.error(chunk);
});

// Read the child process' stdout; parse headers first, then use a dumb pump to get the body out
child.stdout.on("data", function onData(chunk) {
var match;

// Concatenate the new chunk to unprocessed data from the previous chunk
var text = bufferedHeaderText + chunk.toString('ascii');

// Create a *new* regexp; we depend on the lastIndex property to be correct
var lineMatch = /^(.*?)\r?\n/mg;

// Set to true when we're done with the headers
var headersDone = false;

// Keep track of the position in `text`
var offset = 0;

// Try to split text into lines
while ((match = lineMatch.exec(text))) {
offset = lineMatch.lastIndex;

var line = match[1];
if (!line) {
// Empty line, indicates start of body
headersDone = true;
break;
} else if ((match = statusHeaderMatch.exec(line))) {
// HTTP status
statusCode = match[1];
statusText = match[2];
} else if ((match = normalHeaderMatch.exec(line))) {
// Normal HTTP header
respHeaders[match[1]] = match[2];
} else {
// Unsupported, ignore for now
log.error('Got invalid HTTP header from CGI script: ' + line);
}
}

// Done with the headers?
if (!headersDone) {
// Store any unprocessed characters in bufferedHeaderText
bufferedHeaderText = text.slice(offset);
} else {
// Flush the headers
resp.writeHead(statusCode, statusText, respHeaders);

// The remaining bytes in the chunk buffer belong to the response body; send them
var chunkOffset = offset - bufferedHeaderText.length;
if (chunkOffset < chunk.length) {
resp.write(chunk.slice(chunkOffset, chunk.length));
}

// Pump all subsequent chunks straight back to the client
child.stdout.removeListener('data', onData);
sys.pump(child.stdout, resp);
}
});

// Wait for the child process to exit
child.on('exit', function() {
// Close the response
resp.end();

// The child process may be finished even before it has the complete message,
// therefore close the tcp connection itself too.
req.connection.end();
});
}

function close(fd) {
fs.close(fd);
log.debug("closed fd",fd);
Expand Down
5 changes: 5 additions & 0 deletions settings-sample.json
Expand Up @@ -21,6 +21,11 @@
{
"type" : "sjs",
"regexp" : "^.*?\\.sjs(?=$|/)"
},
{
"type" : "cgi",
"regexp" : "^.*?\\.php(?=$|/)",
"exec" : "/usr/bin/php-cgi"
}
]
}

0 comments on commit 77addfc

Please sign in to comment.