Skip to content

Instantly share code, notes, and snippets.

@eventualbuddha
Created January 6, 2011 16:16
Show Gist options
  • Save eventualbuddha/768096 to your computer and use it in GitHub Desktop.
Save eventualbuddha/768096 to your computer and use it in GitHub Desktop.
Capture and replay HTTP requests and responses.
#!/usr/bin/env node
/**
* Capture or replay responses from an HTTP server.
*/
var sys = require('sys'),
path = require('path'),
args = process.argv.slice(2),
// default arg values
port = 9090,
dir = 'assets',
proxyHost = null,
proxyPort = null,
command = null,
isServer = null;
function usage(error) {
var $0 = path.basename(process.argv[1]);
if (error)
sys.error($0+": error: "+error+"\n");
sys.error("Usage: "+$0+" COMMAND [-p PORT] [-d DIR] -h HOST -P PORT");
sys.error("");
sys.error("Commands:");
sys.error(" client Takes incoming requests, proxies them, and stores the responses in DIR.");
sys.error(" server Takes incoming requests and retrieves responses out of DIR.");
sys.error("");
sys.error(" -p, --port Run the file server on this port (defaults to "+port+")");
sys.error(" -d, --dir Capture to or serve from this directory (defaults to "+dir+")");
sys.error("");
sys.error(" -h, --proxy-host");
sys.error(" -P, --proxy-port");
process.exit(error ? 1 : 0);
}
switch (command = args.shift()) {
case 'client': isServer = false; break;
case 'server': isServer = true; break;
case undefined: usage(); break;
default: usage("invalid command "+command); break;
}
while (args.length) {
var arg = args.shift();
var match = arg.match(/^(--\w+[\w-])(?:=(.*))?$/);
if (match) {
// --port=1234, --port, etc
arg = match[1];
args.unshift(match[2]);
}
switch (arg) {
case '-p': case '--port':
var portArg = args.shift();
if (portArg === undefined)
usage("expected port number after "+arg);
port = Number(portArg);
break;
case '-d': case '--dir':
var dirArg = args.shift();
if (dirArg === undefined)
usage("expected directory after "+arg);
dir = dirArg;
break;
case '-h': case '--proxy-host':
var proxyHostArg = args.shift();
if (proxyHostArg === undefined)
usage("expected host after "+arg);
proxyHost = proxyHostArg;
break;
case '-P': case '--proxy-port':
var proxyPortArg = args.shift();
if (proxyPortArg === undefined)
usage("expected port after "+arg);
proxyPort = Number(proxyPortArg);
break;
case '--help':
usage();
break;
default:
usage('unrecognized argument '+arg);
break;
}
}
if (proxyHost === null)
usage("proxy host is required");
if (proxyPort === null)
usage("proxy port is required");
var http = require('http'),
url = require('url'),
fs = require('fs'),
requestCountByURL = {};
function hash(string) {
return new Buffer(string, 'utf8').toString('base64').replace(/\//g, '@');
}
var contentTypeMap = {
txt: 'text/plain',
html: 'text/html',
xml: 'application/xml',
jpg: 'image/jpeg',
png: 'image/png',
tiff: 'image/tiff',
gif: 'image/gif'
};
function mkdir(dir, callback) {
fs.mkdir(dir, 0755, function(err) {
if (err && err.message.indexOf('EEXIST') != 0) {
sys.error('unable to create '+dir+': '+err);
callback(err);
} else {
callback();
}
});
}
mkdir(dir, function(err) {
if (err) return;
http.createServer(function(request, response) {
var count = requestCountByURL[request.url] || 0,
urlRoot = path.join(dir, hash(request.url));
requestCountByURL[request.url] = count + 1;
function write(code, body, headers, root, encoding) {
if (!headers) headers = {};
if (!headers['Content-Type'] && !headers['content-type']) headers['Content-Type'] = contentTypeMap.txt;
response.writeHead(code, headers);
response.end(body, encoding);
var status = request.method+' '+request.url+' '+code+' '+(body||'').length;
if (root) status += ' <= '+root+'\n';
sys.print(status);
}
if (request.url == '/:/toggle') {
isServer = !isServer;
write(200);
return;
}
function serve(root, count, callback) {
if (count < 0) {
callback('no such request found '+root);
return;
}
fs.readFile([root, count, 'info'].join('-'), function(err, data) {
if (err) {
serve(root, count-1, callback);
return;
}
var info = JSON.parse(data);
fs.readFile([root, count, 'body'].join('-'), 'binary', function(err, body) {
if (err) {
serve(root, count-1, callback);
return;
}
write(info.code, body, info.headers, root, 'binary');
callback();
});
});
}
if (isServer) {
// serve the file out of the response store
serve(urlRoot, count, function(err) {
if (err)
write(503, "unable to read response file for request: "+err+"\n", urlRoot);
});
} else {
// proxy the request and store the response
var proxy = http.createClient(proxyPort, proxyHost),
code = null,
contentLength = 0,
proxyRequest = proxy.request(request.method, request.url, request.headers),
fileStream = fs.createWriteStream([urlRoot, count, 'body'].join('-'));
proxyRequest.addListener('response', function(proxyResponse) {
code = proxyResponse.statusCode;
for (var k in proxyResponse.headers)
if (k.toLowerCase() == 'content-length')
contentLength = proxyResponse.headers[k];
proxyResponse.addListener('data', function(chunk) {
response.write(chunk, 'binary');
fileStream.write(chunk, 'binary');
});
proxyResponse.addListener('end', function() {
response.end();
fileStream.end();
});
response.writeHead(proxyResponse.statusCode, proxyResponse.headers);
// save header info
fs.writeFile([urlRoot, count, 'info'].join('-'), JSON.stringify({code: proxyResponse.statusCode, headers: proxyResponse.headers}), function(err) {
if (err)
sys.error('unable to write header info to '+urlRoot+': '+err);
});
sys.print(request.method+' '+request.url+' '+code+' '+contentLength+' => '+urlRoot+'\n');
});
request.addListener('data', function(chunk) {
proxyRequest.write(chunk, 'binary');
});
request.addListener('end', function() {
proxyRequest.end();
});
}
}).listen(port);
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment