Created
April 9, 2011 20:48
-
-
Save carsonmcdonald/911761 to your computer and use it in GitHub Desktop.
node.js SPDY proxy
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var tls = require('tls'); | |
var fs = require('fs'); | |
var Buffer = require('buffer').Buffer; | |
var zlib = require('zlib'); | |
var BufferList = require('bufferlist'); | |
var Binary = require('bufferlist/binary'); | |
var Put = require('put'); | |
var http = require('http'); | |
var options = | |
{ | |
key: fs.readFileSync('test-key.pem'), | |
cert: fs.readFileSync('test-cert.pem'), | |
}; | |
var SPDY_VERSION = 2; | |
var DATA_FRAME_ID = 0; | |
var CONTROL_FRAME_ID = 1; | |
var CF_SYN_STREAM = 1; | |
var CF_SYN_REPLY = 2; | |
var CF_RST_STREAM = 3; | |
var CF_SETTINGS = 4; | |
var CF_NOOP = 5; | |
var CF_PING = 6; | |
var CF_GOAWAY = 7; | |
var CF_HEADERS = 8; | |
var CF_WINDOW_UPDATE = 9; | |
var FLAG_NONE = 0x00; | |
var FLAG_FIN = 0x01; | |
var FLAG_UNIDIRECTIONAL = 0x02; | |
var DICT = "optionsgetheadpostputdeletetraceacceptaccept-charsetaccept-encodingaccept-languageauthorizationexpectfromhostif-modified-sinceif-matchif-none-matchif-rangeif-unmodifiedsincemax-forwardsproxy-authorizationrangerefererteuser-agent100101200201202203204205206300301302303304305306307400401402403404405406407408409410411412413414415416417500501502503504505accept-rangesageetaglocationproxy-authenticatepublicretry-afterservervarywarningwww-authenticateallowcontent-basecontent-encodingcache-controlconnectiondatetrailertransfer-encodingupgradeviawarningcontent-languagecontent-lengthcontent-locationcontent-md5content-rangecontent-typeetagexpireslast-modifiedset-cookieMondayTuesdayWednesdayThursdayFridaySaturdaySundayJanFebMarAprMayJunJulAugSepOctNovDecchunkedtext/htmlimage/pngimage/jpgimage/gifapplication/xmlapplication/xhtmltext/plainpublicmax-agecharset=iso-8859-1utf-8gzipdeflateHTTP/1.1statusversionurl\0"; | |
function extractHeaders(nv_data, nv_entry_count) | |
{ | |
var i, size, name, value, poscount = 2, headers = []; | |
for(i=0; i<nv_entry_count; i++) | |
{ | |
size = (nv_data[poscount++] << 8) + nv_data[poscount++]; | |
name = nv_data.toString('utf-8', poscount, poscount+size); | |
poscount += size; | |
size = (nv_data[poscount++] << 8) + nv_data[poscount++]; | |
value = nv_data.toString('utf-8', poscount, poscount+size); | |
poscount += size; | |
headers[name] = value; // todo need to handle multivalue headers | |
} | |
return headers; | |
} | |
function encodeHeaders(headers) | |
{ | |
// todo add support for multivalue headers | |
var buffer_size = 2, header_count = 0; | |
for(key in headers) | |
{ | |
if(typeof headers[key] == 'string') | |
{ | |
buffer_size += 2; | |
buffer_size += key.length; | |
buffer_size += 2; | |
buffer_size += headers[key].length; | |
header_count++; | |
} | |
} | |
var header_data = new Buffer(buffer_size); | |
var poscount = 0; | |
// todo need to encode the length correctly | |
header_data[poscount++] = 0; | |
header_data[poscount++] = header_count; | |
for(key in headers) | |
{ | |
if(typeof headers[key] == 'string') | |
{ | |
// todo need to encode the length correctly | |
header_data[poscount++] = 0; | |
header_data[poscount++] = key.length; | |
header_data.write(key, poscount, 'binary'); | |
poscount += key.length; | |
// todo need to encode the length correctly | |
header_data[poscount++] = 0; | |
header_data[poscount++] = headers[key].length; | |
header_data.write(headers[key], poscount, 'binary'); | |
poscount += headers[key].length; | |
} | |
} | |
return header_data; | |
} | |
function spdySendData(sock, data, stream_id, flags) | |
{ | |
Put().word8((DATA_FRAME_ID << 7) & 0x80) | |
.word24be(stream_id) // todo this should be combined with the control bit | |
.word8(flags) | |
.word24be(data.length) | |
.put(data) | |
.write(sock); | |
} | |
function spdySendHeaders(sock, headers, stream_id, flags) | |
{ | |
var headers_raw = encodeHeaders(headers); | |
zlib.deflateSetDictionary(new Buffer(DICT, 'binary')); | |
var compressed_headers = zlib.deflate(headers_raw); | |
Put().word8((CONTROL_FRAME_ID << 7) & 0x80) | |
.word8(SPDY_VERSION) | |
.word16be(CF_SYN_REPLY) | |
.word8(flags) | |
.word24be(6 + compressed_headers.length) | |
.word32be(stream_id) // todo this is really 31 bits | |
.pad(2) | |
.put(compressed_headers) | |
.write(sock); | |
} | |
function spdyParser(sock) | |
{ | |
var bufferList = new BufferList; | |
sock.addListener('data', function (data) | |
{ | |
bufferList.push(data); | |
}); | |
// todo this probably needs to repeat until a FIN is seen in frame_flags? | |
var spdyParser = Binary(bufferList) | |
.getWord8('control') | |
.tap(function(vars) | |
{ | |
vars.control_bit = vars.control >> 7 & 0x01; | |
}) | |
.when('control_bit', CONTROL_FRAME_ID, function(vars) | |
{ | |
//console.log('Got control frame'); | |
this.getWord8('version_tmp') | |
.tap(function(vars) | |
{ | |
vars.frame_version = (vars.control & 0x7F << 8) + vars.version_tmp; | |
}) | |
.getWord16be('frame_type') | |
.getWord8('frame_flags') | |
.getWord24be('frame_length') | |
.when('frame_type', CF_SYN_STREAM, function(vars) | |
{ | |
//console.log('Got syn stream: ' + vars.frame_length); | |
this.getWord32be('stream_id') // todo 31 bits | |
.getWord32be('assoc_stream_id') // todo 31 bits | |
.getWord8('priority_tmp') // todo 2 bits | |
.tap(function(vars) | |
{ | |
vars.priority = (vars.priority & 0xC0) >> 6; | |
}) | |
.skip(1) | |
.getBuffer('compressed_headers', vars.frame_length - 10) | |
.tap(function(vars) | |
{ | |
var header_dc = zlib.inflate(vars.compressed_headers, new Buffer(DICT, 'binary')); | |
var header_count = (header_dc[0] << 8) + header_dc[1]; | |
var headers = extractHeaders(header_dc, header_count); | |
spdyParser.emit('spdy-headers', headers, vars.stream_id); | |
}) | |
.when('frame_flags', FLAG_FIN, function(vars) | |
{ | |
console.log('got FLAG_FIN'); | |
}) | |
.exit(); | |
}) | |
.when('frame_type', CF_SYN_REPLY, function(vars) { console.log('CF_SYN_REPLY not implemented yet'); }) | |
.when('frame_type', CF_RST_STREAM, function(vars) { console.log('CF_RST_STREAM not implemented yet'); }) | |
.when('frame_type', CF_SETTINGS, function(vars) { console.log('CF_SETTINGS not implemented yet'); }) | |
.when('frame_type', CF_NOOP, function(vars) { console.log('CF_NOOP not implemented yet'); }) | |
.when('frame_type', CF_PING, function(vars) { console.log('CF_PING not implemented yet'); }) | |
.when('frame_type', CF_GOAWAY, function(vars) { console.log('CF_GOAWAY not implemented yet'); }) | |
.when('frame_type', CF_HEADERS, function(vars) { console.log('CF_HEADERS not implemented yet'); }) | |
.when('frame_type', CF_WINDOW_UPDATE, function(vars) { console.log('CF_WINDOW_UPDATE not implemented yet'); }) | |
.exit(); | |
}) | |
.when('control_bit', DATA_FRAME_ID, function(vars) | |
{ | |
console.log('Got data frame'); | |
// todo | |
this.exit(); | |
}) | |
.end(); | |
return spdyParser; | |
} | |
function proxyRequest(headers, stream_id, sock) | |
{ | |
var proxy = http.createClient(80, 'arewefastyet.com'); | |
var proxy_request = proxy.request(headers['method'], headers['url'], {'host': 'arewefastyet.com'}); | |
console.log('Making proxy request: ' + headers['method'] + ' and ' + headers['url']); | |
proxy_request.addListener('response', function (proxy_response) | |
{ | |
proxy_response.addListener('data', function(chunk) | |
{ | |
spdySendData(sock, chunk, stream_id, FLAG_NONE); | |
}); | |
proxy_response.addListener('end', function() | |
{ | |
spdySendData(sock, '', stream_id, FLAG_FIN); | |
sock.end(); | |
}); | |
response_headers = proxy_response.headers; | |
response_headers['version'] = 'HTTP/' + proxy_response.httpVersion; | |
response_headers['status'] = '' + proxy_response.statusCode; | |
spdySendHeaders(sock, response_headers, stream_id, FLAG_NONE); | |
}); | |
proxy_request.end(); | |
} | |
var ccount = 0; | |
tls.createServer(options, function (s) | |
{ | |
ccount++; | |
console.log('got client connection: ' + ccount); | |
s.addListener('end', function () { console.log('Stream end: ' + ccount); ccount--; }); | |
var parser = spdyParser(s); | |
parser.addListener('spdy-headers', function(headers, stream_id) | |
{ | |
proxyRequest(headers, stream_id, s); | |
}); | |
}).listen(8787); | |
console.log('127.0.0.1:8787'); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment