--- 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);
+ });
+ });
});