Bug 1283862 - Update node-spdy to 1.32.5. r?mcmanus draft
authorMasatoshi Kimura <VYV03354@nifty.ne.jp>
Fri, 01 Jul 2016 21:52:14 +0900
changeset 383160 c4c76f133ccad737fa4dd3420021d7571dbb20c0
parent 383145 fdcee57b4e4f66a82831ab01e61500da98a858e8
child 524410 654c20e3660f9d88b80047a327580b575c5b8ecb
push id21953
push userVYV03354@nifty.ne.jp
push dateFri, 01 Jul 2016 14:53:02 +0000
reviewersmcmanus
bugs1283862
milestone50.0a1
Bug 1283862 - Update node-spdy to 1.32.5. r?mcmanus MozReview-Commit-ID: GfAXRxNMslW
testing/xpcshell/node-spdy/README.md
testing/xpcshell/node-spdy/lib/spdy/client.js
testing/xpcshell/node-spdy/lib/spdy/connection.js
testing/xpcshell/node-spdy/lib/spdy/protocol/framer.js
testing/xpcshell/node-spdy/lib/spdy/protocol/parser.js
testing/xpcshell/node-spdy/lib/spdy/response.js
testing/xpcshell/node-spdy/lib/spdy/stream.js
testing/xpcshell/node-spdy/lib/spdy/utils.js
testing/xpcshell/node-spdy/package.json
testing/xpcshell/node-spdy/test/unit/connect-test.js
testing/xpcshell/node-spdy/test/unit/stream-test.js
--- a/testing/xpcshell/node-spdy/README.md
+++ b/testing/xpcshell/node-spdy/README.md
@@ -26,17 +26,17 @@ var options = {
   autoSpdy31: false
 };
 
 var server = spdy.createServer(options, function(req, res) {
   res.writeHead(200);
   res.end('hello world!');
 });
 
-server.listen(443);
+server.listen(3000);
 ```
 
 Client:
 ```javascript
 var spdy = require('spdy');
 var http = require('http');
 
 var agent = spdy.createAgent({
@@ -75,17 +75,17 @@ var spdy = require('spdy'),
 var options = { /* the same as above */ };
 
 var app = express();
 
 app.use(/* your favorite middleware */);
 
 var server = spdy.createServer(options, app);
 
-server.listen(443);
+server.listen(3000);
 ```
 
 ## API
 
 API is compatible with `http` and `https` module, but you can use another
 function as base class for SPDYServer.
 
 ```javascript
@@ -115,19 +115,45 @@ spdy.createServer(options, function(req,
   var stream = res.push('/main.js', headers);
   stream.on('acknowledge', function() {
   });
   stream.on('error', function() {
   });
   stream.end('alert("hello from push stream!");');
 
   res.end('<script src="/main.js"></script>');
-}).listen(443);
+}).listen(3000);
 ```
 
+The method is also avaliable when using SPDY with an Express server:
+
+```javascript
+var app = express();
+
+var server = spdy.createServer(options, app);
+
+app.get('/', function(req, res) {
+  var headers = { 'content-type': 'application/javascript' };
+  res.push('/main.js', headers, function(err, stream) {
+    stream.on('acknowledge', function() {
+    });
+
+    stream.on('error', function() {
+    });
+
+    stream.end('alert("hello from push stream!");');
+  });
+
+  res.end('<script src="/main.js"></script>');
+});
+
+server.listen(3000);
+```
+
+
 Push is accomplished via the `push()` method invoked on the current response
 object (this works for express.js response objects as well).  The format of the
 `push()` method is:
 
 `.push('full or relative url', { ... headers ... }, optional priority, callback)`
 
 You can use either full ( `http://host/path` ) or relative ( `/path` ) urls with
 `.push()`. `headers` are the same as for regular response object. `callback`
--- a/testing/xpcshell/node-spdy/lib/spdy/client.js
+++ b/testing/xpcshell/node-spdy/lib/spdy/client.js
@@ -166,17 +166,16 @@ proto.createConnection = function create
   var stream = new spdy.Stream(state.connection, {
     id: state.id,
     priority: options.priority || 7,
     client: true,
     decompress: options.spdy.decompress == undefined ? true :
                                                        options.spdy.decompress
   });
   state.id += 2;
-  state.connection._addStream(stream);
 
   return stream;
 };
 
 //
 // ### function close (callback)
 // #### @callback {Function}
 // Close underlying socket and terminate all streams
--- a/testing/xpcshell/node-spdy/lib/spdy/connection.js
+++ b/testing/xpcshell/node-spdy/lib/spdy/connection.js
@@ -164,16 +164,19 @@ Connection.prototype._init = function in
     this.socket.on('drain', function ondrain() {
       self.emit('drain');
     });
   }
 
   // for both legacy and non-legacy, when our socket is ready for writes again,
   //   drain the sinks in case they were queuing due to socketBuffering.
   this.socket.on('drain', function () {
+    if (!state.socketBuffering)
+      return;
+
     state.socketBuffering = false;
     Object.keys(state.streams).forEach(function(id) {
       state.streams[id]._drainSink(0);
     });
   });
 };
 
 //
@@ -293,17 +296,16 @@ Connection.prototype._handleSynStream = 
       if (frame.type === 'RST_STREAM')
         return;
       this._rst(frame.id, constants.rst.INVALID_STREAM);
       return;
     }
   }
 
   var stream = new Stream(this, frame);
-  this._addStream(stream);
 
   // Associate streams
   if (associated) {
     stream.associated = associated;
   }
 
   // TODO(indutny) handle stream limit
   this.emit('stream', stream);
@@ -551,16 +553,18 @@ Connection.prototype._handlePing = funct
   var ours = state.isServer && (id % 2 === 0) ||
              !state.isServer && (id % 2 === 1);
 
   // Handle incoming PING
   if (!ours) {
     state.framer.pingFrame(id, function(err, frame) {
       if (err)
         return self.emit('error', err);
+
+      self.emit('ping', id);
       self.write(frame);
     });
     return;
   }
 
   // Handle reply PING
   if (!state.pings[id])
     return;
--- a/testing/xpcshell/node-spdy/lib/spdy/protocol/framer.js
+++ b/testing/xpcshell/node-spdy/lib/spdy/protocol/framer.js
@@ -1,14 +1,16 @@
 var spdy = require('../../spdy');
 var util = require('util');
 var Buffer = require('buffer').Buffer;
 var EventEmitter = require('events').EventEmitter;
 var constants = require('./').constants;
 
+var empty = new Buffer(0);
+
 function Framer() {
   EventEmitter.call(this);
 
   this.version = null;
   this.deflate = null;
   this.inflate = null;
   this.debug = false;
 };
@@ -305,17 +307,17 @@ Framer.prototype.headersFrame = function
 Framer.prototype.dataFrame = function dataFrame(id, fin, data, callback) {
   if (!this.version) {
     return this.on('version', function() {
       this.dataFrame(id, fin, data, callback);
     });
   }
 
   if (!fin && !data.length)
-    return callback(null, []);
+    return callback(null, empty);
 
   var frame = new Buffer(8 + data.length);
 
   frame.writeUInt32BE(id & 0x7fffffff, 0, true);
   frame.writeUInt32BE(data.length & 0x00ffffff, 4, true);
   frame.writeUInt8(fin ? 0x01 : 0x0, 4, true);
 
   if (data.length)
--- a/testing/xpcshell/node-spdy/lib/spdy/protocol/parser.js
+++ b/testing/xpcshell/node-spdy/lib/spdy/protocol/parser.js
@@ -54,16 +54,50 @@ parser.create = function create(connecti
 
 //
 // ### function destroy ()
 // Just a stub.
 //
 Parser.prototype.destroy = function destroy() {
 };
 
+Parser.prototype._slice = function _slice(waiting) {
+  var buffer = new Buffer(waiting);
+  var sliced = 0;
+  var offset = 0;
+
+  while (waiting > offset && sliced < this.buffer.length) {
+    var chunk = this.buffer[sliced++],
+        overmatched = false;
+
+    // Copy chunk into `buffer`
+    if (chunk.length > waiting - offset) {
+      chunk.copy(buffer, offset, 0, waiting - offset);
+
+      this.buffer[--sliced] = chunk.slice(waiting - offset);
+      this.buffered += this.buffer[sliced].length;
+
+      overmatched = true;
+    } else {
+      chunk.copy(buffer, offset);
+    }
+
+    // Move offset and decrease amount of buffered data
+    offset += chunk.length;
+    this.buffered -= chunk.length;
+
+    if (overmatched) break;
+  }
+
+  // Remove used buffers
+  this.buffer = this.buffer.slice(sliced);
+
+  return buffer;
+};
+
 //
 // ### function _write (data, encoding, cb)
 // #### @data {Buffer} chunk of data
 // #### @encoding {Null} encoding
 // #### @cb {Function} callback
 // Writes or buffers data to parser
 //
 Parser.prototype._write = function write(data, encoding, cb) {
@@ -78,58 +112,46 @@ Parser.prototype._write = function write
 
   // Notify caller about state (for piping)
   if (this.paused) {
     this.needDrain = true;
     cb();
     return false;
   }
 
+  if (this.needDrain) {
+    // Mark parser as drained
+    this.needDrain = false;
+    this.emit('drain');
+  }
+
   // We shall not do anything until we get all expected data
   if (this.buffered < this.waiting) {
-    if (this.needDrain) {
-      // Mark parser as drained
-      this.needDrain = false;
-      this.emit('drain');
+    // Emit DATA by chunks as they come
+    if (this.buffered !== 0 &&
+        this.state.header &&
+        !this.state.header.control) {
+      var buffer = this._slice(this.buffered);
+      this.waiting -= buffer.length;
+
+      this.emit('frame', {
+        type: 'DATA',
+        id: this.state.header.id,
+        fin: false,
+        compressed: (this.state.header.flags & 0x02) === 0x02,
+        data: buffer
+      });
     }
 
     cb();
     return;
   }
 
-  var self = this,
-      buffer = new Buffer(this.waiting),
-      sliced = 0,
-      offset = 0;
-
-  while (this.waiting > offset && sliced < this.buffer.length) {
-    var chunk = this.buffer[sliced++],
-        overmatched = false;
-
-    // Copy chunk into `buffer`
-    if (chunk.length > this.waiting - offset) {
-      chunk.copy(buffer, offset, 0, this.waiting - offset);
-
-      this.buffer[--sliced] = chunk.slice(this.waiting - offset);
-      this.buffered += this.buffer[sliced].length;
-
-      overmatched = true;
-    } else {
-      chunk.copy(buffer, offset);
-    }
-
-    // Move offset and decrease amount of buffered data
-    offset += chunk.length;
-    this.buffered -= chunk.length;
-
-    if (overmatched) break;
-  }
-
-  // Remove used buffers
-  this.buffer = this.buffer.slice(sliced);
+  var self = this;
+  var buffer = this._slice(this.waiting);
 
   // Executed parser for buffered data
   this.paused = true;
   var sync = true;
   this.execute(this.state, buffer, function (err, waiting) {
     // Propagate errors
     if (err) {
       // And unpause once execution finished
@@ -227,16 +249,17 @@ Parser.prototype.execute = function exec
     }
 
     function onFrame(err, frame) {
       if (err) return callback(err);
 
       self.emit('frame', frame);
 
       state.type = 'frame-head';
+      state.header = null;
       callback(null, 8);
     };
   }
 };
 
 
 //
 // ### function parseHeader (data)
--- a/testing/xpcshell/node-spdy/lib/spdy/response.js
+++ b/testing/xpcshell/node-spdy/lib/spdy/response.js
@@ -160,17 +160,16 @@ exports.push = function push(url, header
     type: 'SYN_STREAM',
     id: id,
     associated: socket._spdyState.id,
     priority: priority,
     headers: {}
   });
 
   stream.associated = socket;
-  socket.connection._addStream(stream);
 
   socket._lock(function() {
     this._spdyState.framer.streamFrame(
       id,
       this._spdyState.id,
       {
         method: 'GET',
         path: url,
--- a/testing/xpcshell/node-spdy/lib/spdy/stream.js
+++ b/testing/xpcshell/node-spdy/lib/spdy/stream.js
@@ -2,16 +2,18 @@ var spdy = require('../spdy');
 var zlib = require('zlib');
 var utils = spdy.utils;
 var assert = require('assert');
 var util = require('util');
 var stream = require('stream');
 var Buffer = require('buffer').Buffer;
 var constants = spdy.protocol.constants;
 
+var empty = new Buffer(0);
+
 if (!/^v(0\.10|0\.8|0\.9)\./.test(process.version))
   var httpCommon = require('_http_common');
 
 
 //
 // ### function Stream (connection, options)
 // #### @connection {Connection} SPDY Connection
 // #### @options {Object} Stream options
@@ -45,24 +47,34 @@ function Stream(connection, options) {
   state.finishAttached = false;
   state.decompress = options.decompress;
   state.decompressor = null;
 
   // Store id
   state.id = options.id;
   state.associated = options.associated;
   state.headers = options.headers || {};
+
+  // Protocol standard compliance:
+  // Client does not have to send this header, we must assume,
+  // that it can handle compression
+  if (connection._spdyState.isServer && !state.associated &&
+      !state.headers['accept-encoding']) {
+    state.headers['accept-encoding'] = 'gzip, deflate';
+  }
+
   if (connection._spdyState.xForward !== null)
     state.headers['x-forwarded-for'] = connection._spdyState.xForward;
 
   // Increment counters
   state.isPush = !connection._spdyState.isServer && state.associated;
   connection._spdyState.counters.streamCount++;
   if (state.isPush)
     connection._spdyState.counters.pushCount++;
+  connection._addStream(this);
 
   // Useful for PUSH streams
   state.scheme = state.headers.scheme;
   state.host = state.headers.host;
 
   if (state.isPush && state.headers['content-encoding']) {
     var compression = state.headers['content-encoding'];
 
@@ -106,18 +118,35 @@ function Stream(connection, options) {
 
   this._init();
 };
 util.inherits(Stream, utils.DuplexStream);
 exports.Stream = Stream;
 
 // Parser lookup methods
 Stream.prototype._parserHeadersComplete = function parserHeadersComplete() {
+  var method;
   if (this.parser)
-    return this.parser.onHeadersComplete || this.parser[1];
+    method = this.parser.onHeadersComplete || this.parser[1];
+  if (!method || !utils.isArgvParser)
+    return method;
+
+  return function onHeadersCompleteWrap(info) {
+    // NOTE: shouldKeepAlive = false
+    return method.call(this,
+                       info.versionMajor,
+                       info.versionMinor,
+                       info.headers,
+                       info.method,
+                       info.url,
+                       info.statusCode,
+                       info.statusMessage,
+                       info.upgrade,
+                       false);
+  };
 };
 
 Stream.prototype._parserBody = function parserBody() {
   if (this.parser)
     return this.parser.onBody || this.parser[2];
 };
 
 Stream.prototype._parserMessageComplete = function parserMessageComplete() {
@@ -149,17 +178,17 @@ Stream.prototype._init = function init()
   });
 
   // Handle half-close
   this.once('finish', function onfinish() {
     if (state.chunkedWrite)
       return this.once('_chunkDone', onfinish);
 
     var self = this;
-    this._writeData(true, [], function() {
+    this._writeData(true, empty, function() {
       state.closedBy.us = true;
       if (state.sinkBuffer.length !== 0)
         return;
       if (utils.isLegacy)
         self.emit('full-finish');
       self._handleClose();
     });
   });
@@ -711,17 +740,16 @@ Stream.prototype._parseClientRequest = f
       priority: self.priority
     }, headers, function(err, frame) {
       if (err) {
         self._unlock();
         return self.emit('error', err);
       }
       connection.write(frame);
       self._unlock();
-      connection._addStream(self);
 
       self.emit('_spdyRequest');
       state.initialized = true;
       if (cb)
         cb();
     })
   });
 
--- a/testing/xpcshell/node-spdy/lib/spdy/utils.js
+++ b/testing/xpcshell/node-spdy/lib/spdy/utils.js
@@ -7,16 +7,18 @@ var stream = require('stream'),
 
 // Export streams related stuff
 utils.isLegacy = !stream.Duplex;
 if (utils.isLegacy)
   utils.DuplexStream = stream;
 else
   utils.DuplexStream = stream.Duplex;
 
+utils.isArgvParser = !/^v(0\.12|0\.11|0\.10|0\.8|0\.9)\./.test(process.version);
+
 //
 // ### function createDeflate (version, compression)
 // #### @version {Number} SPDY version
 // #### @compression {Boolean} whether to enable compression
 // Creates deflate stream with SPDY dictionary
 //
 utils.createDeflate = function createDeflate(version, compression) {
   var deflate = zlib.createDeflate({
@@ -39,17 +41,17 @@ utils.createDeflate = function createDef
 // ### function createInflate (version)
 // #### @version {Number} SPDY version
 // Creates inflate stream with SPDY dictionary
 //
 utils.createInflate = function createInflate(version) {
   var inflate = zlib.createInflate({
     dictionary: spdy.protocol.dictionary[version],
     flush: zlib.Z_SYNC_FLUSH,
-    windowBits: 15
+    windowBits: 0
   });
 
   // Define lock information early
   inflate.locked = false;
   inflate.lockQueue = [];
   if (spdy.utils.isLegacy)
     inflate._flush = zlib.Z_SYNC_FLUSH;
 
--- a/testing/xpcshell/node-spdy/package.json
+++ b/testing/xpcshell/node-spdy/package.json
@@ -1,24 +1,24 @@
 {
   "name": "spdy",
-  "version": "1.29.1",
+  "version": "1.32.5",
   "description": "Implementation of the SPDY protocol on node.js.",
   "license": "MIT",
   "keywords": [
     "spdy"
   ],
   "repository": {
     "type": "git",
     "url": "git://github.com/indutny/node-spdy.git"
   },
   "homepage": "https://github.com/indutny/node-spdy",
   "bugs": {
     "email": "node-spdy+bugs@indutny.com",
-    "url": "https://github.com/indunty/node-spdy/issues"
+    "url": "https://github.com/indutny/node-spdy/issues"
   },
   "author": "Fedor Indutny <fedor.indutny@gmail.com>",
   "contributors": [
     "Chris Storm <github@eeecooks.com>",
     "François de Metz <francois@2metz.fr>",
     "Ilya Grigorik <ilya@igvita.com>",
     "Roberto Peon",
     "Tatsuhiro Tsujikawa",
--- a/testing/xpcshell/node-spdy/test/unit/connect-test.js
+++ b/testing/xpcshell/node-spdy/test/unit/connect-test.js
@@ -12,16 +12,23 @@ suite('A SPDY Server / Connect', functio
   var fox = 'The quick brown fox jumps over the lazy dog';
 
   setup(function(done) {
     server = spdy.createServer(keys, function(req, res) {
       var comp = req.url === '/gzip' ? zlib.createGzip() :
                  req.url === '/deflate' ? zlib.createDeflate() :
                  null;
 
+      if (req.url == '/headerTest') {
+        if (req.headers['accept-encoding'] == 'gzip, deflate')
+          return res.end(fox);
+        else
+          return res.end();
+      }
+
       // Terminate connection gracefully
       if (req.url === '/goaway')
         req.socket.connection.end();
 
       if (!comp)
         return res.end(fox);
 
       res.writeHead(200, { 'Content-Encoding' : req.url.slice(1) });
@@ -197,9 +204,36 @@ suite('A SPDY Server / Connect', functio
     });
     req.end();
 
     agent._spdyState.socket.once('close', function() {
       assert.equal(total, fox);
       done();
     });
   });
+
+  test('should add accept-encoding header to request headers, ' +
+           'if none present',
+       function(done) {
+    var agent = spdy.createAgent({
+      host: '127.0.0.1',
+      port: PORT,
+      rejectUnauthorized: false
+    });
+
+    var req = https.request({
+        path: '/headerTest',
+        method: 'GET',
+        agent: agent,
+      }, function(res) {
+        var total = '';
+        res.on('data', function(chunk){
+          total += chunk;
+        });
+        res.once('end', function() {
+          agent.close();
+          assert.equal(total, fox);
+          done();
+        });
+      });
+      req.end();
+  });
 });
--- a/testing/xpcshell/node-spdy/test/unit/stream-test.js
+++ b/testing/xpcshell/node-spdy/test/unit/stream-test.js
@@ -363,9 +363,49 @@ suite('A SPDY Server / Stream', function
       pair.server.req.on('end', function() {
         pair.server.res.end();
         done();
       });
 
       pair.client.req.end();
     });
   }
+
+  test('it should not attempt to send empty arrays', function(done) {
+    var server = spdy.createServer(keys);
+    var agent;
+
+    server.on('request', function(req, res) {
+      setTimeout(function() {
+        res.end();
+        done();
+        server.close();
+        agent.close();
+      }, 100);
+    });
+
+    server.listen(PORT, function() {
+      agent = spdy.createAgent({
+        port: PORT,
+        rejectUnauthorized: false
+      }).on('error', function(err) {
+        done(err);
+      });
+
+      var body = new Buffer([1,2,3,4]);
+
+      var opts = {
+        method: 'POST',
+        headers: {
+          'Host': 'localhost',
+          'Content-Length': body.length
+        },
+        path: '/some-url',
+        agent: agent
+      };
+
+      var req = https.request(opts, function(res) {
+      }).on('error', function(err) {
+      });
+      req.end(body);
+    });
+  });
 });