Skip to content

Instantly share code, notes, and snippets.

@carsonmcdonald
Created April 9, 2011 20:48
Show Gist options
  • Save carsonmcdonald/911761 to your computer and use it in GitHub Desktop.
Save carsonmcdonald/911761 to your computer and use it in GitHub Desktop.
node.js SPDY proxy
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