Thursday, May 12, 2011

Problem Solving with node-spdy

‹prev | My Chain | next›

Today I continue (and hopefully complete) my quest to decompress a second SYN_STREAM packet from Chrome that is crashing the SPDY gem based server:
/home/chris/repos/spdy/lib/spdy/compressor.rb:35:in `inflate': invalid stream (RuntimeError)
from /home/chris/repos/spdy/lib/spdy/parser.rb:51:in `unpack_control'
from /home/chris/repos/spdy/lib/spdy/parser.rb:81:in `try_parse'
from /home/chris/repos/spdy/lib/spdy/parser.rb:19:in `<<'
from ./npn_spdy_server.rb:47:in `receive_data'
from /home/chris/.rvm/gems/ruby-1.9.2-p180@spdy/gems/eventmachine-1.0.0.beta.3/lib/eventmachine.rb:206:in `run_machine'
from /home/chris/.rvm/gems/ruby-1.9.2-p180@spdy/gems/eventmachine-1.0.0.beta.3/lib/eventmachine.rb:206:in `run'
from ./npn_spdy_server.rb:66:in `<main>'
As mentioned in a comment the other day, the SPDY spec for zlib compression seems to imply that the same zlib context needs to be re-used when inflating subsequent frames:
The entire contents of the name/value header block is compressed using zlib deflate. There is a single zlib stream (context) for all name value pairs in one direction on a connection.
Currently, the SPDY gem creates a new zlib context for each packet.

Last night I was unable to figure out how to re-use zlib contexts in ruby. A cursory examination of the node-spdy node.js packages seems to indicate that this is a solved problem there.

Before I head to node-land, I stay briefly in ruby-world to make sure I have the correct packets. When I print the first packet to $stderr, it looks like:
"\x80\x02\x00\x01\x01\x00\x01\x0E\x00\x00\x00\x01\x00\x00\x00\x00\x00\x008\xEA\xDF\xA2Q\xB2b\xE0b`\x83\xA4\x17\x06{\xB8\vu0,\xD6\xAE@\x17\xCD\xCD\xB1.\xB45\xD0\xB3\xD4\xD1\xD2\xD7\x02\xB3,\x18\xF8Ps,\x83\x9Cg\xB0?\xD4"
When I unpack it as hex (unpack('H*')), it looks a little larger:
800200010100010e0000000100000000000038eadfa251b262e0626083a417067bb80b75302cd6ae4017cdcdb12eb435d0b3d4d1d2d702b32c18f850732c839c67b03fd43d3a600781d599eb40d41b33f0a3e5690641908b75a04ed6294e49ce80ab81250306bed43cddd0609dd43ca8a52ca03ccec00f4a083920a61530e3191830b0e5020b97fc14066677d71006b662607acc4d6560cd28292928666006799c519f810b915b19d27df3ab32737212f54df50c1434008a3034b456f0c9cc2bad50a8b0308b3733d15470047a3e353c35c93bb344dfd4d844cf18a8ccdb23c4d747472127333b55c13d35393b5f53c1390358eca4ea1b1ae9017d6a620452169c9896589409d5c4c00e0d7c060e589c00000000ffff
I have mostly been using the former. A quick inspection of the packets with Wireshark (I sure am glad I figure out how to do that) bears out the idea that the hex dump is the correct one:



Similarly the second packet, in both forms looks like:
"\x80\x02\x00\x01\x01\x00\x00'\x00\x00\x00\x03\x00\x00\x00\x00\x80\x00B\x8A\x02f``\x0E\xAD`\xE4\xD1OK,\xCB\x04f3"
800200010100002700000003000000008000428a026660600ead60e4d14f4b2ccb0466333d2031584214000000ffff
(again, I know that these are SPDY SYN_STREAM frames because the fourth octet is 01)

To get those hex octets into node, I use a little Ruby magic to convert to a comma-separated list of octets:
ruby-1.9.2-p0 > # Just the data part of the packet:
ruby-1.9.2-p0 > d2 = "428a026660600ead60e4d14f4b2ccb0466333d2031584214000000ffff"
ruby-1.9.2-p0 > d2.split(/(..)/).reject{|x|x==''}.map{|x|"0x#{x}"}.join(',')
=> "0x42,0x8a,0x02,0x66,0x60,0x60,0x0e,0xad,0x60,0xe4,0xd1,0x4f,0x4b,0x2c,0xcb,0x04,0x66,0x33,0x3d,0x20,0x31,0x58,0x42,0x14,0x00,0x00,0x00,0xff,0xff"
I can then copy & paste that into a node REPL to get a buffer object:
> var d2 = new Buffer([0x42,0x8a,0x02,0x66,0x60,0x60,0x0e,0xad,0x60,0xe4,0xd1,0x4f,0x4b,0x2c,0xcb,0x04,0x66,0x33,0x3d,0x20,0x31,0x58,0x42,0x14,0x00,0x00,0x00,0xff,0xff]);
Next I install edge (aka 0.5.0-pre) node.js and link it to edge openssl (needed for SPDY).

The zlib context code from node-spdy is as follows:
var Buffer = require('buffer').Buffer,
ZLibContext = require('zlibcontext').ZLibContext;

var flatDictStr = [
'optionsgetheadpostputdeletetraceacceptaccept-charsetaccept-encodingaccept-',
'languageauthorizationexpectfromhostif-modified-sinceif-matchif-none-matchi',
'f-rangeif-unmodifiedsincemax-forwardsproxy-authorizationrangerefererteuser',
'-agent10010120020120220320420520630030130230330430530630740040140240340440',
'5406407408409410411412413414415416417500501502503504505accept-rangesageeta',
'glocationproxy-authenticatepublicretry-afterservervarywarningwww-authentic',
'ateallowcontent-basecontent-encodingcache-controlconnectiondatetrailertran',
'sfer-encodingupgradeviawarningcontent-languagecontent-lengthcontent-locati',
'oncontent-md5content-rangecontent-typeetagexpireslast-modifiedset-cookieMo',
'ndayTuesdayWednesdayThursdayFridaySaturdaySundayJanFebMarAprMayJunJulAugSe',
'pOctNovDecchunkedtext/htmlimage/pngimage/jpgimage/gifapplication/xmlapplic',
'ation/xhtmltext/plainpublicmax-agecharset=iso-8859-1utf-8gzipdeflateHTTP/1',
'.1statusversionurl'
].join(''),
flatDict = new Buffer(flatDictStr.length + 1);

flatDict.write(flatDictStr, 'ascii');
flatDict[flatDict.length - 1] = 0;

var ZLib = exports.ZLib = function() {
this.context = new ZLibContext(flatDict);
};

exports.createZLib = function() {
return new ZLib();
};

ZLib.prototype.deflate = function(buffer) {
return this.context.deflate(buffer);
};

ZLib.prototype.inflate = function(buffer) {
return this.context.inflate(buffer);
};
The code is well organized easy to follow (although where the zlib package ends and zlibcontext begins is a bit confused). It pulls in the node Buffer and npm ZlibContext objects. It then builds the standard SPDY zlib dictionary (also described in the HEADERS section of Draft #2 of the SPDY spec).

The remainder of the code is used to build up ZLib objects to hold contexts for inflating compressed data. That sounds exactly like what I want.

For my purposes here, I just need a single context, so I copy the dictionary code and make a single zlib context manually:
var Buffer = require('buffer').Buffer,
ZLibContext = require('zlibcontext').ZLibContext;

var flatDictStr = [
'optionsgetheadpostputdeletetraceacceptaccept-charsetaccept-encodingaccept-',
'languageauthorizationexpectfromhostif-modified-sinceif-matchif-none-matchi',
'f-rangeif-unmodifiedsincemax-forwardsproxy-authorizationrangerefererteuser',
'-agent10010120020120220320420520630030130230330430530630740040140240340440',
'5406407408409410411412413414415416417500501502503504505accept-rangesageeta',
'glocationproxy-authenticatepublicretry-afterservervarywarningwww-authentic',
'ateallowcontent-basecontent-encodingcache-controlconnectiondatetrailertran',
'sfer-encodingupgradeviawarningcontent-languagecontent-lengthcontent-locati',
'oncontent-md5content-rangecontent-typeetagexpireslast-modifiedset-cookieMo',
'ndayTuesdayWednesdayThursdayFridaySaturdaySundayJanFebMarAprMayJunJulAugSe',
'pOctNovDecchunkedtext/htmlimage/pngimage/jpgimage/gifapplication/xmlapplic',
'ation/xhtmltext/plainpublicmax-agecharset=iso-8859-1utf-8gzipdeflateHTTP/1',
'.1statusversionurl'
].join(''),
flatDict = new Buffer(flatDictStr.length + 1);

flatDict.write(flatDictStr, 'ascii');
flatDict[flatDict.length - 1] = 0;

var context = new ZLibContext(flatDict);
Next I create Buffer objects to hold the two SYN_STREAM packets:
var d1 = new Buffer([0x38,0xea,0xdf,0xa2,0x51,0xb2,0x62,0xe0,0x62,0x60,0x83,0xa4,0x17,0x06,0x7b,0xb8,0x0b,0x75,0x30,0x2c,0xd6,0xae,0x40,0x17,0xcd,0xcd,0xb1,0x2e,0xb4,0x35,0xd0,0xb3,0xd4,0xd1,0xd2,0xd7,0x02,0xb3,0x2c,0x18,0xf8,0x50,0x73,0x2c,0x83,0x9c,0x67,0xb0,0x3f,0xd4,0x3d,0x3a,0x60,0x07,0x81,0xd5,0x99,0xeb,0x40,0xd4,0x1b,0x33,0xf0,0xa3,0xe5,0x69,0x06,0x41,0x90,0x8b,0x75,0xa0,0x4e,0xd6,0x29,0x4e,0x49,0xce,0x80,0xab,0x81,0x25,0x03,0x06,0xbe,0xd4,0x3c,0xdd,0xd0,0x60,0x9d,0xd4,0x3c,0xa8,0xa5,0x2c,0xa0,0x3c,0xce,0xc0,0x0f,0x4a,0x08,0x39,0x20,0xa6,0x15,0x30,0xe3,0x19,0x18,0x30,0xb0,0xe5,0x02,0x0b,0x97,0xfc,0x14,0x06,0x66,0x77,0xd7,0x10,0x06,0xb6,0x62,0x60,0x7a,0xcc,0x4d,0x65,0x60,0xcd,0x28,0x29,0x29,0x28,0x66,0x60,0x06,0x79,0x9c,0x51,0x9f,0x81,0x0b,0x91,0x5b,0x19,0xd2,0x7d,0xf3,0xab,0x32,0x73,0x72,0x12,0xf5,0x4d,0xf5,0x0c,0x14,0x34,0x00,0x8a,0x30,0x34,0xb4,0x56,0xf0,0xc9,0xcc,0x2b,0xad,0x50,0xa8,0xb0,0x30,0x8b,0x37,0x33,0xd1,0x54,0x70,0x04,0x7a,0x3e,0x35,0x3c,0x35,0xc9,0x3b,0xb3,0x44,0xdf,0xd4,0xd8,0x44,0xcf,0x18,0xa8,0xcc,0xdb,0x23,0xc4,0xd7,0x47,0x47,0x21,0x27,0x33,0x3b,0x55,0xc1,0x3d,0x35,0x39,0x3b,0x5f,0x53,0xc1,0x39,0x03,0x58,0xec,0xa4,0xea,0x1b,0x1a,0xe9,0x01,0x7d,0x6a,0x62,0x04,0x52,0x16,0x9c,0x98,0x96,0x58,0x94,0x09,0xd5,0xc4,0xc0,0x0e,0x0d,0x7c,0x06,0x0e,0x58,0x9c,0x00,0x00,0x00,0x00,0xff,0xff]);

var d2 = new Buffer([0x42,0x8a,0x02,0x66,0x60,0x60,0x0e,0xad,0x60,0xe4,0xd1,0x4f,0x4b,0x2c,0xcb,0x04,0x66,0x33,0x3d,0x20,0x31,0x58,0x42,0x14,0x00,0x00,0x00,0xff,0xff]);
With that, I am ready to do some inflating!
var nv1 = context.inflate(d1);
var nv2 = context.inflate(d2);
The first packet, as a string, is:
> nv1.toString();
'\u0000\n\u0000\u0006accept\u0000?text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\u0000\u000eaccept-charset\u0000\u001eISO-8859-1,utf-8;q=0.7,*;q=0.3\u0000\u000faccept-encoding\u0000\u0011gzip,deflate,sdch\u0000\u000faccept-language\u0000\u000een-US,en;q=0.8\u0000\u0004host\u0000\u000flocalhost:10000\u0000\u0006method\u0000\u0003GET\u0000\u0006scheme\u0000\u0005https\u0000\u0003url\u0000\u0001/\u0000\nuser-agent\u0000gMozilla/5.0 (X11; Linux x86_64) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.30 Safari/534.30\u0000\u0007version\u0000\bHTTP/1.1'
Nothing new there, just a name/value frame. First 16 bits (\u0000\n) represent the number of NV pairs in the frame. \n is 10 in ASCII, so there are 10 NV pairs there. The number preceding each name or value is the lenght (\u0000\u0006 == 6 => 6 characters in "accept"). That has always worked.

What has not been working in the ruby SPDY server is the second packet. So what has been causing me all of this trouble? What have I literally lost sleep over for the past three nights?
> nv2.toString();
'\u0000\n\u0000\u0006accept\u0000\u0003*/*\u0000\u000eaccept-charset\u0000\u001eISO-8859-1,utf-8;q=0.7,*;q=0.3\u0000\u000faccept-encoding\u0000\u0011gzip,deflate,sdch\u0000\u000faccept-language\u0000\u000een-US,en;q=0.8\u0000\u0004host\u0000\u000flocalhost:10000\u0000\u0006method\u0000\u0003GET\u0000\u0006scheme\u0000\u0005https\u0000\u0003url\u0000\f/favicon.ico\u0000\nuser-agent\u0000gMozilla/5.0 (X11; Linux x86_64) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.30 Safari/534.30\u0000\u0007version\u0000\bHTTP/1.1'
A freaking favicon.

And yes, creating a new zlib context and inflating does not work any better in javascript land than it does in ruby world:
> var context = new ZLibContext(flatDict);
> var nv2 = context.inflate(d2);
Error: incorrect header check
at [object Context]:1:19
at Interface.<anonymous> (repl.js:168:22)
at Interface.emit (events.js:64:17)
at Interface._onLine (readline.js:153:10)
at Interface._line (readline.js:408:8)
at Interface._ttyWrite (readline.js:585:14)
at ReadStream.<anonymous> (readline.js:73:12)
at ReadStream.emit (events.js:81:20)
at ReadStream._emitKey (tty_posix.js:307:10)
at ReadStream.onData (tty_posix.js:70:12)
A freakin' favicon! Gah!

Day #14

1 comment: