Comparing version 0.0.1 to 0.0.2
@@ -5,6 +5,6 @@ // The framer consists of two [Transform Stream][1] subclasses that operate in [object mode][2]: | ||
// [2]: http://nodejs.org/api/stream.html#stream_new_stream_readable_options | ||
var Transform = require('stream').Transform | ||
var Transform = require('stream').Transform; | ||
exports.Serializer = Serializer | ||
exports.Deserializer = Deserializer | ||
exports.Serializer = Serializer; | ||
exports.Deserializer = Deserializer; | ||
@@ -14,34 +14,47 @@ // Serializer | ||
// | ||
// Frame Objects +-----------------------------+ Buffers | ||
// * * * * * * * ---> | Serializer Transform Stream | ---> * * * * | ||
// +-----------------------------+ | ||
// Frame Objects | ||
// * * * * * * * --+--------------------------- | ||
// | | | ||
// v v Buffers | ||
// [] -----> Payload Ser. --[buffers]--> Header Ser. --> * * * * | ||
// empty adds payload adds header | ||
// array buffers buffer | ||
function Serializer() { | ||
Transform.call(this, { objectMode: true }) | ||
Transform.call(this, { objectMode: true }); | ||
} | ||
Serializer.prototype = Object.create(Transform.prototype, { constructor: { value: Serializer } }) | ||
Serializer.prototype = Object.create(Transform.prototype, { constructor: { value: Serializer } }); | ||
// When there's an incoming frame object, it first generates the frame type specific part of the | ||
// frame (payload), and then then header part which holds fields that are common to all frame types | ||
// (like the length of the payload). | ||
// frame (payload), and then then adds the header part which holds fields that are common to all | ||
// frame types (like the length of the payload). | ||
Serializer.prototype._transform = function _transform(frame, encoding, done) { | ||
var payload = Serializer[frame.type](frame) | ||
frame.length = payload.length | ||
var header = Serializer.commonHeader(frame) | ||
if (!(frame.type in Serializer)) { | ||
throw new Error('Unknown frame type: ' + frame.type); | ||
} | ||
this.push(header) | ||
this.push(payload) | ||
done() | ||
} | ||
var buffers = []; | ||
Serializer[frame.type](frame, buffers); | ||
Serializer.commonHeader(frame, buffers); | ||
for (var i = 0; i < buffers.length; i++) { | ||
this.push(buffers[i]); | ||
} | ||
done(); | ||
}; | ||
// Deserializer | ||
// ------------ | ||
// | ||
// Buffers +-------------------------------+ Frame Objects | ||
// * * * * ---> | Deserializer Transform Stream | ---> * * * * * * * | ||
// +-------------------------------+ | ||
// Buffers | ||
// * * * * --------+------------------------- | ||
// | | | ||
// v v Frame Objects | ||
// {} -----> Header Des. --{frame}--> Payload Des. --> * * * * * * * | ||
// empty adds parsed adds parsed | ||
// object header properties payload properties | ||
function Deserializer() { | ||
Transform.call(this, { objectMode: true }) | ||
this._next(8) | ||
Transform.call(this, { objectMode: true }); | ||
this._next(8); | ||
} | ||
@@ -56,6 +69,9 @@ Deserializer.prototype = Object.create(Transform.prototype, { constructor: { value: Deserializer } }) | ||
Deserializer.prototype._next = function(size) { | ||
this._cursor = 0 | ||
this._buffer = new Buffer(size) | ||
this._waiting_for_header = !this._waiting_for_header | ||
} | ||
this._cursor = 0; | ||
this._buffer = new Buffer(size); | ||
this._waiting_for_header = !this._waiting_for_header; | ||
if (this._waiting_for_header) { | ||
this._frame = {}; | ||
} | ||
}; | ||
@@ -65,3 +81,3 @@ // Parsing an incoming buffer is an iterative process because it can hold multiple frames if it's | ||
Deserializer.prototype._transform = function _transform(chunk, encoding, done) { | ||
var cursor = 0 | ||
var cursor = 0; | ||
@@ -71,6 +87,6 @@ while(cursor < chunk.length) { | ||
// chunk, then only a part of it is copied. | ||
var to_copy = Math.min(chunk.length - cursor, this._buffer.length - this._cursor) | ||
chunk.copy(this._buffer, this._cursor, cursor, cursor + to_copy) | ||
this._cursor += to_copy | ||
cursor += to_copy | ||
var to_copy = Math.min(chunk.length - cursor, this._buffer.length - this._cursor); | ||
chunk.copy(this._buffer, this._cursor, cursor, cursor + to_copy); | ||
this._cursor += to_copy; | ||
cursor += to_copy; | ||
@@ -83,16 +99,17 @@ // When `_buffer` is full, it's content gets parsed either as header or payload depending on | ||
// deserializer waits for the specified length payload. | ||
this._header = Deserializer.commonHeader(this._buffer) | ||
this._next(this._header.length) | ||
Deserializer.commonHeader(this._buffer, this._frame); | ||
this._next(this._frame.length); | ||
} else { | ||
// If it's payload then the the frame object is assembled and then gets pushed out. | ||
// If it's payload then the the frame object is finalized and then gets pushed out. | ||
// Unknown frame types are ignored. | ||
if (this._header.type) { | ||
var frame = Deserializer[this._header.type](this._buffer) | ||
frame.type = this._header.type | ||
frame.flags = this._header.flags | ||
frame.stream = this._header.stream | ||
this.push(frame) | ||
if (this._frame.type) { | ||
try { | ||
Deserializer[this._frame.type](this._buffer, this._frame); | ||
this.push(this._frame); | ||
} catch(error) { | ||
this.emit('error', error); | ||
} | ||
} | ||
this._next(8) | ||
this._next(8); | ||
} | ||
@@ -102,4 +119,4 @@ } | ||
done() | ||
} | ||
done(); | ||
}; | ||
@@ -148,49 +165,54 @@ // [Frame Header](http://http2.github.io/http2-spec/#FrameHeader) | ||
var frame_types = [] | ||
var frame_types = []; | ||
var frame_flags = {} | ||
var frame_flags = {}; | ||
Serializer.commonHeader = function writeCommonHeader(frame) { | ||
var data = new Buffer(8) | ||
Serializer.commonHeader = function writeCommonHeader(frame, buffers) { | ||
var header_buffer = new Buffer(8); | ||
if (frame.length > 65535) throw new Error('Too large frame: ' + frame.length + ' bytes') | ||
data.writeUInt16BE(frame.length, 0) | ||
var size = 0; | ||
for (var i = 0; i < buffers.length; i++) size += buffers[i].length; | ||
if (size > 65535) { | ||
throw new Error('Too large frame: ' + size + ' bytes'); | ||
} | ||
header_buffer.writeUInt16BE(size, 0); | ||
var type_id = frame_types.indexOf(frame.type) | ||
if (type_id === -1) throw new Error('Unknown frame type: ' + frame.type) | ||
data.writeUInt8(type_id, 2) | ||
var type_id = frame_types.indexOf(frame.type); // If we are here then the type is valid for sure | ||
header_buffer.writeUInt8(type_id, 2); | ||
var flag_byte = 0 | ||
var flag_byte = 0; | ||
for (var flag in frame.flags) { | ||
var position = frame_flags[frame.type].indexOf(flag) | ||
if (position === -1) throw new Error('Unknown flag for frame type ' + frame.type + ': ' + flag) | ||
if (frame.flags[flag]) flag_byte |= (1 << position) | ||
var position = frame_flags[frame.type].indexOf(flag); | ||
if (position === -1) { | ||
throw new Error('Unknown flag for frame type ' + frame.type + ': ' + flag); | ||
} | ||
if (frame.flags[flag]) { | ||
flag_byte |= (1 << position); | ||
} | ||
} | ||
data.writeUInt8(flag_byte, 3) | ||
header_buffer.writeUInt8(flag_byte, 3); | ||
if (frame.stream > 0x7fffffff) throw new Error('Too large stream ID: ' + frame.stream) | ||
data.writeUInt32BE(frame.stream || 0, 4) | ||
if (frame.stream > 0x7fffffff) { | ||
throw new Error('Too large stream ID: ' + frame.stream); | ||
} | ||
header_buffer.writeUInt32BE(frame.stream || 0, 4); | ||
return data | ||
} | ||
buffers.unshift(header_buffer); | ||
}; | ||
Deserializer.commonHeader = function readCommonHeader(buffer) { | ||
var frame = {} | ||
Deserializer.commonHeader = function readCommonHeader(buffer, frame) { | ||
frame.length = buffer.readUInt16BE(0); | ||
frame.length = buffer.readUInt16BE(0) | ||
frame.type = frame_types[buffer.readUInt8(2)]; | ||
frame.type = frame_types[buffer.readUInt8(2)] | ||
frame.flags = {} | ||
var flag_byte = buffer.readUInt8(3) | ||
var defined_flags = frame_flags[frame.type] | ||
frame.flags = {}; | ||
var flag_byte = buffer.readUInt8(3); | ||
var defined_flags = frame_flags[frame.type]; | ||
for (var i = 0; i < defined_flags.length; i++) { | ||
frame.flags[defined_flags[i]] = Boolean(flag_byte & (1 << i)) | ||
frame.flags[defined_flags[i]] = Boolean(flag_byte & (1 << i)); | ||
} | ||
frame.stream = buffer.readUInt32BE(4) & 0x7fffffff | ||
frame.stream = buffer.readUInt32BE(4) & 0x7fffffff; | ||
}; | ||
return frame | ||
} | ||
// Frame types | ||
@@ -205,27 +227,44 @@ // =========== | ||
// | ||
// The DATA frame does not define any type-specific flags. | ||
// The DATA frame defines the following flags: | ||
// | ||
// * END_STREAM (0x1): | ||
// Bit 1 being set indicates that this frame is the last that the endpoint will send for the | ||
// identified stream. | ||
// * RESERVED (0x2): | ||
// Bit 2 is reserved for future use. | ||
frame_types[0x0] = 'DATA' | ||
frame_types[0x0] = 'DATA'; | ||
frame_flags['DATA'] = [] | ||
frame_flags.DATA = ['END_STREAM', 'RESERVED']; | ||
Serializer['DATA'] = function writeData(frame) { | ||
return frame.data | ||
} | ||
Serializer.DATA = function writeData(frame, buffers) { | ||
buffers.push(frame.data); | ||
}; | ||
Deserializer['DATA'] = function readData(buffer) { | ||
return { data: buffer } | ||
} | ||
Deserializer.DATA = function readData(buffer, frame) { | ||
frame.data = buffer; | ||
}; | ||
// [HEADERS+PRIORITY](http://http2.github.io/http2-spec/#HEADERS) | ||
// [HEADERS](http://http2.github.io/http2-spec/#HEADERS) | ||
// -------------------------------------------------------------- | ||
// | ||
// The HEADERS+PRIORITY frame (type=0x1) allows the sender to set header fields and stream priority | ||
// at the same time. | ||
// The HEADERS frame (type=0x1) allows the sender to create a stream. | ||
// | ||
// HEADERS+PRIORITY uses the same flags as the HEADERS frame. | ||
// The HEADERS frame defines the following flags: | ||
// | ||
// * END_STREAM (0x1): | ||
// Bit 1 being set indicates that this frame is the last that the endpoint will send for the | ||
// identified stream. | ||
// * RESERVED (0x2): | ||
// Bit 2 is reserved for future use. | ||
// * END_HEADERS (0x4): | ||
// The END_HEADERS bit indicates that this frame contains the entire payload necessary to provide | ||
// a complete set of headers. | ||
// * PRIORITY (0x8): | ||
// Bit 4 being set indicates that the first four octets of this frame contain a single reserved | ||
// bit and a 31-bit priority. | ||
frame_types[0x1] = 'HEADERS+PRIORITY' | ||
frame_types[0x1] = 'HEADERS'; | ||
frame_flags['HEADERS+PRIORITY'] = ['CONTINUES'] | ||
frame_flags.HEADERS = ['END_STREAM', 'RESERVED', 'END_HEADERS', 'PRIORITY']; | ||
@@ -235,3 +274,3 @@ // 0 1 2 3 | ||
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | ||
// |X| Priority (31) | | ||
// |X| (Optional) Priority (31) | | ||
// +-+-------------------------------------------------------------+ | ||
@@ -241,18 +280,21 @@ // | Header Block (*) ... | ||
// | ||
// The HEADERS+PRIORITY frame is identical to the HEADERS frame, preceded by a | ||
// single reserved bit and a 31-bit priority. | ||
// The payload of a HEADERS frame contains a Headers Block | ||
Serializer['HEADERS+PRIORITY'] = function writeHeadersPriority(frame) { | ||
var data = new Buffer(4 + frame.data.length) | ||
data.writeUInt32BE(frame.priority & 0x7fffffff, 0) | ||
frame.data.copy(data, 4) | ||
return data | ||
} | ||
Serializer.HEADERS = function writeHeadersPriority(frame, buffers) { | ||
if (frame.flags.PRIORITY) { | ||
var buffer = new Buffer(4); | ||
buffer.writeUInt32BE(frame.priority & 0x7fffffff, 0); | ||
buffers.push(buffer); | ||
} | ||
buffers.push(frame.data); | ||
}; | ||
Deserializer['HEADERS+PRIORITY'] = function readHeadersPriority(buffer) { | ||
return { | ||
priority: buffer.readUInt32BE(0) & 0x7fffffff, | ||
data: buffer.slice(4) | ||
Deserializer.HEADERS = function readHeadersPriority(buffer, frame) { | ||
if (frame.flags.PRIORITY) { | ||
frame.priority = buffer.readUInt32BE(0) & 0x7fffffff; | ||
frame.data = buffer.slice(4); | ||
} else { | ||
frame.data = buffer; | ||
} | ||
} | ||
}; | ||
@@ -266,5 +308,5 @@ // [PRIORITY](http://http2.github.io/http2-spec/#PRIORITY) | ||
frame_types[0x2] = 'PRIORITY' | ||
frame_types[0x2] = 'PRIORITY'; | ||
frame_flags['PRIORITY'] = [] | ||
frame_flags.PRIORITY = []; | ||
@@ -279,13 +321,11 @@ // 0 1 2 3 | ||
Serializer['PRIORITY'] = function writePriority(frame) { | ||
var data = new Buffer(4) | ||
data.writeUInt32BE(frame.priority, 0) | ||
return data | ||
} | ||
Serializer.PRIORITY = function writePriority(frame, buffers) { | ||
var buffer = new Buffer(4); | ||
buffer.writeUInt32BE(frame.priority, 0); | ||
buffers.push(buffer); | ||
}; | ||
Deserializer['PRIORITY'] = function readPriority(buffer) { | ||
return { | ||
priority: buffer.readUInt32BE(0) | ||
} | ||
} | ||
Deserializer.PRIORITY = function readPriority(buffer, frame) { | ||
frame.priority = buffer.readUInt32BE(0); | ||
}; | ||
@@ -299,5 +339,5 @@ // [RST_STREAM](http://http2.github.io/http2-spec/#RST_STREAM) | ||
frame_types[0x3] = 'RST_STREAM' | ||
frame_types[0x3] = 'RST_STREAM'; | ||
frame_flags['RST_STREAM'] = [] | ||
frame_flags.RST_STREAM = []; | ||
@@ -313,13 +353,11 @@ // 0 1 2 3 | ||
Serializer['RST_STREAM'] = function writeRstStream(frame) { | ||
var data = new Buffer(4) | ||
data.writeUInt32BE(error_codes.indexOf(frame.error), 0) | ||
return data | ||
} | ||
Serializer.RST_STREAM = function writeRstStream(frame, buffers) { | ||
var buffer = new Buffer(4); | ||
buffer.writeUInt32BE(error_codes.indexOf(frame.error), 0); | ||
buffers.push(buffer); | ||
}; | ||
Deserializer['RST_STREAM'] = function readRstStream(buffer) { | ||
return { | ||
error: error_codes[buffer.readUInt32BE(0)] | ||
} | ||
} | ||
Deserializer.RST_STREAM = function readRstStream(buffer, frame) { | ||
frame.error = error_codes[buffer.readUInt32BE(0)]; | ||
}; | ||
@@ -332,11 +370,7 @@ // [SETTINGS](http://http2.github.io/http2-spec/#SETTINGS) | ||
// | ||
// The SETTINGS frame defines the following flag: | ||
// | ||
// * CLEAR_PERSISTED (0x2): | ||
// Bit 2 being set indicates a request to clear any previously persisted settings before | ||
// processing the settings. | ||
// The SETTINGS frame does not define any flags. | ||
frame_types[0x4] = 'SETTINGS' | ||
frame_types[0x4] = 'SETTINGS'; | ||
frame_flags['SETTINGS'] = ['CLEAR_PERSISTED'] | ||
frame_flags.SETTINGS = []; | ||
@@ -361,46 +395,53 @@ // The payload of a SETTINGS frame consists of zero or more settings. Each setting consists of an | ||
Serializer['SETTINGS'] = function writeSettings(frame) { | ||
var settings = [] | ||
Serializer.SETTINGS = function writeSettings(frame, buffers) { | ||
var settings = [], settings_left = Object.keys(frame.settings); | ||
defined_settings.forEach(function(setting, id) { | ||
if (setting.name in frame.settings) { | ||
var value = frame.settings[setting.name] | ||
settings.push({ id: id, value: setting.flag ? value & 0x1 : value }) | ||
settings_left.splice(settings_left.indexOf(setting.name), 1); | ||
var value = frame.settings[setting.name]; | ||
settings.push({ id: id, value: setting.flag ? Boolean(value) : value }); | ||
} | ||
}) | ||
}); | ||
if (settings_left.length !== 0) { | ||
throw new Error('Unknown settings: ' + settings_left.join(', ')) | ||
} | ||
var buffer = new Buffer(settings.length * 8) | ||
var buffer = new Buffer(settings.length * 8); | ||
for (var i = 0; i < settings.length; i++) { | ||
buffer.writeUInt32BE(settings[i].id & 0xffffff, i*8) | ||
buffer.writeUInt32BE(settings[i].value, i*8 + 4) | ||
buffer.writeUInt32BE(settings[i].id & 0xffffff, i*8); | ||
buffer.writeUInt32BE(settings[i].value, i*8 + 4); | ||
} | ||
return buffer | ||
} | ||
buffers.push(buffer); | ||
}; | ||
Deserializer['SETTINGS'] = function readSettings(buffer) { | ||
var settings = {} | ||
Deserializer.SETTINGS = function readSettings(buffer, frame) { | ||
frame.settings = {}; | ||
if (buffer.length % 8 !== 0) { | ||
throw new Error('Invalid SETTINGS frame.'); | ||
} | ||
for (var i = 0; i < buffer.length / 8; i++) { | ||
var id = buffer.readUInt32BE(i*8) & 0xffffff | ||
var setting = defined_settings[id] | ||
var value = buffer.readUInt32BE(i*8 + 4) | ||
if (setting.name in settings) continue | ||
settings[setting.name] = setting.flag ? Boolean(value & 0x1) : value | ||
var id = buffer.readUInt32BE(i*8) & 0xffffff; | ||
var setting = defined_settings[id]; | ||
var value = buffer.readUInt32BE(i*8 + 4); | ||
if (!setting || setting.name in frame.settings) { | ||
continue; | ||
} | ||
frame.settings[setting.name] = setting.flag ? Boolean(value & 0x1) : value; | ||
} | ||
return { | ||
settings: settings | ||
} | ||
} | ||
return frame; | ||
}; | ||
// The following settings are defined: | ||
var defined_settings = [] | ||
var defined_settings = []; | ||
// * SETTINGS_MAX_CONCURRENT_STREAMS (4): | ||
// indicates the maximum number of concurrent streams that the sender will allow. | ||
defined_settings[4] = { name: 'SETTINGS_MAX_CONCURRENT_STREAMS', flag: false } | ||
defined_settings[4] = { name: 'SETTINGS_MAX_CONCURRENT_STREAMS', flag: false }; | ||
// * SETTINGS_INITIAL_WINDOW_SIZE (7): | ||
// indicates the sender's initial stream window size (in bytes) for new streams. | ||
defined_settings[7] = { name: 'SETTINGS_INITIAL_WINDOW_SIZE', flag: false } | ||
defined_settings[7] = { name: 'SETTINGS_INITIAL_WINDOW_SIZE', flag: false }; | ||
@@ -411,3 +452,3 @@ // * SETTINGS_FLOW_CONTROL_OPTIONS (10): | ||
// bits are reserved. | ||
defined_settings[10] = { name: 'SETTINGS_FLOW_CONTROL_OPTIONS', flag: true } | ||
defined_settings[10] = { name: 'SETTINGS_FLOW_CONTROL_OPTIONS', flag: true }; | ||
@@ -420,7 +461,11 @@ // [PUSH_PROMISE](http://http2.github.io/http2-spec/#PUSH_PROMISE) | ||
// | ||
// PUSH_PROMISE uses the same flags as the HEADERS frame. | ||
// The PUSH_PROMISE frame defines the following flags: | ||
// | ||
// * END_PUSH_PROMISE (0x1): | ||
// The END_PUSH_PROMISE bit indicates that this frame contains the entire payload necessary to | ||
// provide a complete set of headers. | ||
frame_types[0x5] = 'PUSH_PROMISE' | ||
frame_types[0x5] = 'PUSH_PROMISE'; | ||
frame_flags['PUSH_PROMISE'] = ['CONTINUES'] | ||
frame_flags.PUSH_PROMISE = ['END_PUSH_PROMISE']; | ||
@@ -439,15 +484,13 @@ // 0 1 2 3 | ||
Serializer['PUSH_PROMISE'] = function writePushPromise(frame) { | ||
var data = new Buffer(4 + frame.data.length) | ||
data.writeUInt32BE(frame.promised_stream & 0x7fffffff, 0) | ||
frame.data.copy(data, 4) | ||
return data | ||
} | ||
Serializer.PUSH_PROMISE = function writePushPromise(frame, buffers) { | ||
var buffer = new Buffer(4); | ||
buffer.writeUInt32BE(frame.promised_stream & 0x7fffffff, 0); | ||
buffers.push(buffer); | ||
buffers.push(frame.data); | ||
}; | ||
Deserializer['PUSH_PROMISE'] = function readPushPromise(buffer) { | ||
return { | ||
promised_stream: buffer.readUInt32BE(0) & 0x7fffffff, | ||
data: buffer.slice(4) | ||
} | ||
} | ||
Deserializer.PUSH_PROMISE = function readPushPromise(buffer, frame) { | ||
frame.promised_stream = buffer.readUInt32BE(0) & 0x7fffffff; | ||
frame.data = buffer.slice(4); | ||
}; | ||
@@ -465,18 +508,20 @@ // [PING](http://http2.github.io/http2-spec/#PING) | ||
frame_types[0x6] = 'PING' | ||
frame_types[0x6] = 'PING'; | ||
frame_flags['PING'] = ['PONG'] | ||
frame_flags.PING = ['PONG']; | ||
// In addition to the frame header, PING frames MUST contain 8 additional octets of opaque data. | ||
Serializer['PING'] = function writePing(frame) { | ||
var payload = frame.data | ||
if (!payload || payload.length !== 8) throw new Error('PING frames must carry an 8 byte payload.') | ||
return payload | ||
Serializer.PING = function writePing(frame, buffers) { | ||
if (!frame.data || frame.data.length !== 8) { | ||
throw new Error('PING frames must carry an 8 byte payload.'); | ||
} | ||
buffers.push(frame.data); | ||
} | ||
Deserializer['PING'] = function readPing(buffer) { | ||
return { | ||
data: buffer | ||
Deserializer.PING = function readPing(buffer, frame) { | ||
if (buffer.length !== 8) { | ||
throw new Error('Invalid size PING frame.'); | ||
} | ||
frame.data = buffer; | ||
} | ||
@@ -489,7 +534,7 @@ | ||
// | ||
// The GOAWAY frame does not define any type-specific flags. | ||
// The GOAWAY frame does not define any flags. | ||
frame_types[0x7] = 'GOAWAY' | ||
frame_types[0x7] = 'GOAWAY'; | ||
frame_flags['GOAWAY'] = [] | ||
frame_flags.GOAWAY = []; | ||
@@ -511,41 +556,14 @@ // 0 1 2 3 | ||
Serializer['GOAWAY'] = function writeGoaway(frame) { | ||
var data = new Buffer(8) | ||
data.writeUInt32BE(frame.last_stream & 0x7fffffff, 0) | ||
data.writeUInt32BE(error_codes.indexOf(frame.error), 4) | ||
return data | ||
} | ||
Serializer.GOAWAY = function writeGoaway(frame, buffers) { | ||
var buffer = new Buffer(8); | ||
buffer.writeUInt32BE(frame.last_stream & 0x7fffffff, 0); | ||
buffer.writeUInt32BE(error_codes.indexOf(frame.error), 4); | ||
buffers.push(buffer); | ||
}; | ||
Deserializer['GOAWAY'] = function readGoaway(buffer) { | ||
return { | ||
last_stream: buffer.readUInt32BE(0) & 0x7fffffff, | ||
error: error_codes[buffer.readUInt32BE(4)] | ||
} | ||
} | ||
Deserializer.GOAWAY = function readGoaway(buffer, frame) { | ||
frame.last_stream = buffer.readUInt32BE(0) & 0x7fffffff; | ||
frame.error = error_codes[buffer.readUInt32BE(4)]; | ||
}; | ||
// [HEADERS](http://http2.github.io/http2-spec/#HEADERS) | ||
// ----------------------------------------------------- | ||
// | ||
// The HEADERS frame (type=0x8) provides header fields for a stream. | ||
// | ||
// Additional type-specific flags for the HEADERS frame are: | ||
// | ||
// * CONTINUES (0x2): | ||
// The CONTINUES bit indicates that this frame does not contain the entire payload necessary to | ||
// provide a complete set of headers. | ||
frame_types[0x8] = 'HEADERS' | ||
frame_flags['HEADERS'] = ['CONTINUES'] | ||
// The payload of a HEADERS frame contains a Headers Block (Section 3.7). | ||
Serializer['HEADERS'] = function writeHeaders(frame) { | ||
return frame.data | ||
} | ||
Deserializer['HEADERS'] = function readHeaders(buffer) { | ||
return { data: buffer } | ||
} | ||
// [WINDOW_UPDATE](http://http2.github.io/http2-spec/#WINDOW_UPDATE) | ||
@@ -556,11 +574,11 @@ // ----------------------------------------------------------------- | ||
// | ||
// The following additional flags are defined for the WINDOW_UPDATE frame: | ||
// The WINDOW_UPDATE frame defines the following flags: | ||
// | ||
// * END_FLOW_CONTROL (0x2): | ||
// Bit 2 being set indicates that flow control for the identified stream | ||
// * END_FLOW_CONTROL (0x1): | ||
// Bit 1 being set indicates that flow control for the identified stream | ||
// or connection has been ended; subsequent frames do not need to be flow controlled. | ||
frame_types[0x9] = 'WINDOW_UPDATE' | ||
frame_types[0x9] = 'WINDOW_UPDATE'; | ||
frame_flags['WINDOW_UPDATE'] = ['END_FLOW_CONTROL'] | ||
frame_flags.WINDOW_UPDATE = ['END_FLOW_CONTROL']; | ||
@@ -572,22 +590,12 @@ // The payload of a WINDOW_UPDATE frame is a 32-bit value indicating the additional number of bytes | ||
Serializer['WINDOW_UPDATE'] = function writeWindowUpdate(frame) { | ||
var data = new Buffer(4) | ||
data.writeUInt32BE(frame.window_size & 0x7fffffff, 0) | ||
return data | ||
} | ||
Serializer.WINDOW_UPDATE = function writeWindowUpdate(frame, buffers) { | ||
var buffer = new Buffer(4); | ||
buffer.writeUInt32BE(frame.window_size & 0x7fffffff, 0); | ||
buffers.push(buffer); | ||
}; | ||
Deserializer['WINDOW_UPDATE'] = function readWindowUpdate(buffer) { | ||
return { | ||
window_size: buffer.readUInt32BE(0) & 0x7fffffff | ||
} | ||
} | ||
Deserializer.WINDOW_UPDATE = function readWindowUpdate(buffer, frame) { | ||
frame.window_size = buffer.readUInt32BE(0) & 0x7fffffff; | ||
}; | ||
// Common Flags | ||
// ------------ | ||
// | ||
// The least significant bit (0x1) - the FINAL bit - is defined for all frame types as an indication | ||
// that this frame is the last the endpoint will send for the identified stream. | ||
for (var type in frame_flags) frame_flags[type].unshift('FINAL') | ||
// [Error Codes](http://http2.github.io/http2-spec/#ErrorCodes) | ||
@@ -607,2 +615,2 @@ // ------------------------------------------------------------ | ||
'COMPRESSION_ERROR' | ||
] | ||
]; |
{ | ||
"name": "http2", | ||
"version": "0.0.1", | ||
"version": "0.0.2", | ||
"description": "An HTTP/2 server implementation", | ||
@@ -30,4 +30,4 @@ "main": "index.js", | ||
"author": "Gábor Molnár <gabor@molnar.es> (http://gabor.molnar.es)", | ||
"license": "BSD", | ||
"license": "MIT", | ||
"readmeFilename": "README.md" | ||
} |
node-http2 | ||
========== | ||
An HTTP/2 server implementation for node.js | ||
An HTTP/2 server implementation for node.js, developed as a [Google Summer of Code project](https://google-melange.appspot.com/gsoc/project/google/gsoc2013/molnarg/5001). | ||
Status | ||
====== | ||
I post weekly status updates [on my blog](http://gabor.molnar.es/blog/categories/google-summer-of-code/). Short version: framing layer 70% ready. | ||
Installation | ||
============ | ||
Using npm: | ||
``` | ||
npm install http2 | ||
``` | ||
Documentation | ||
============= | ||
The developer documentation is generated using [docco](http://jashkenas.github.io/docco/), and is located in the `doc` directory. API documentation is coming later. | ||
Running the tests | ||
================= | ||
To run the tests, first install [mocha](http://visionmedia.github.io/mocha/) and [chai](http://visionmedia.github.io/mocha/) (`npm install mocha chai`) and then run `npm test`. | ||
The tests are written in BDD style, so they are a good starting point to understand the code. | ||
License | ||
======= | ||
The MIT License | ||
Copyright (C) 2013 Gábor Molnár <gabor@molnar.es> | ||
@@ -1,19 +0,18 @@ | ||
var expect = require('chai').expect | ||
var expect = require('chai').expect; | ||
var framer = require('../lib/framer') | ||
, Serializer = framer.Serializer | ||
, Deserializer = framer.Deserializer | ||
, Deserializer = framer.Deserializer; | ||
var frame_types = { | ||
'DATA': ['data'], | ||
'HEADERS+PRIORITY': ['priority', 'data'], | ||
'PRIORITY': ['priority'], | ||
'RST_STREAM': ['error'], | ||
'SETTINGS': ['settings'], | ||
'PUSH_PROMISE': ['promised_stream', 'data'], | ||
'PING': ['data'], | ||
'GOAWAY': ['last_stream', 'error'], | ||
'HEADERS': ['data'], | ||
'WINDOW_UPDATE': ['window_size'] | ||
} | ||
DATA: ['data'], | ||
HEADERS: ['priority', 'data'], | ||
PRIORITY: ['priority'], | ||
RST_STREAM: ['error'], | ||
SETTINGS: ['settings'], | ||
PUSH_PROMISE: ['promised_stream', 'data'], | ||
PING: ['data'], | ||
GOAWAY: ['last_stream', 'error'], | ||
WINDOW_UPDATE: ['window_size'] | ||
}; | ||
@@ -23,3 +22,3 @@ var test_frames = [{ | ||
type: 'DATA', | ||
flags: { 'FINAL': false }, | ||
flags: { END_STREAM: false, RESERVED: false }, | ||
stream: 10, | ||
@@ -32,7 +31,19 @@ length: 4, | ||
buffer: new Buffer('0004' + '00' + '00' + '0000000A' + '12345678', 'hex') | ||
}, { | ||
frame: { | ||
type: 'HEADERS+PRIORITY', | ||
flags: { 'FINAL': false, 'CONTINUES': false }, | ||
type: 'HEADERS', | ||
flags: { END_STREAM: false, RESERVED: false, END_HEADERS: false, PRIORITY: false }, | ||
stream: 15, | ||
length: 4, | ||
data: new Buffer('12345678', 'hex') | ||
}, | ||
buffer: new Buffer('0004' + '01' + '00' + '0000000F' + '12345678', 'hex') | ||
}, { | ||
frame: { | ||
type: 'HEADERS', | ||
flags: { END_STREAM: false, RESERVED: false, END_HEADERS: false, PRIORITY: true }, | ||
stream: 15, | ||
length: 8, | ||
@@ -43,7 +54,8 @@ | ||
}, | ||
buffer: new Buffer('0008' + '01' + '00' + '0000000F' + '00000003' + '12345678', 'hex') | ||
buffer: new Buffer('0008' + '01' + '08' + '0000000F' + '00000003' + '12345678', 'hex') | ||
}, { | ||
frame: { | ||
type: 'PRIORITY', | ||
flags: { 'FINAL': false }, | ||
flags: { }, | ||
stream: 10, | ||
@@ -55,6 +67,7 @@ length: 4, | ||
buffer: new Buffer('0004' + '02' + '00' + '0000000A' + '00000003', 'hex') | ||
}, { | ||
frame: { | ||
type: 'RST_STREAM', | ||
flags: { 'FINAL': false }, | ||
flags: { }, | ||
stream: 10, | ||
@@ -66,6 +79,7 @@ length: 4, | ||
buffer: new Buffer('0004' + '03' + '00' + '0000000A' + '00000002', 'hex') | ||
}, { | ||
frame: { | ||
type: 'SETTINGS', | ||
flags: { 'FINAL': false, 'CLEAR_PERSISTED': false }, | ||
flags: { }, | ||
stream: 10, | ||
@@ -83,6 +97,7 @@ length: 24, | ||
'00' + '00000A' + '00000001', 'hex') | ||
}, { | ||
frame: { | ||
type: 'PUSH_PROMISE', | ||
flags: { 'FINAL': false, 'CONTINUES': false }, | ||
flags: { END_PUSH_PROMISE: false }, | ||
stream: 15, | ||
@@ -95,6 +110,7 @@ length: 8, | ||
buffer: new Buffer('0008' + '05' + '00' + '0000000F' + '00000003' + '12345678', 'hex') | ||
}, { | ||
frame: { | ||
type: 'PING', | ||
flags: { 'FINAL': false, 'PONG': false }, | ||
flags: { PONG: false }, | ||
stream: 15, | ||
@@ -106,6 +122,7 @@ length: 8, | ||
buffer: new Buffer('0008' + '06' + '00' + '0000000F' + '1234567887654321', 'hex') | ||
}, { | ||
frame: { | ||
type: 'GOAWAY', | ||
flags: { 'FINAL': false }, | ||
flags: { }, | ||
stream: 10, | ||
@@ -118,16 +135,7 @@ length: 8, | ||
buffer: new Buffer('0008' + '07' + '00' + '0000000A' + '12345678' + '00000001', 'hex') | ||
}, { | ||
frame: { | ||
type: 'HEADERS', | ||
flags: { 'FINAL': false, 'CONTINUES': false }, | ||
stream: 10, | ||
length: 4, | ||
data: new Buffer('12345678', 'hex') | ||
}, | ||
buffer: new Buffer('0004' + '08' + '00' + '0000000A' + '12345678', 'hex') | ||
}, { | ||
frame: { | ||
type: 'WINDOW_UPDATE', | ||
flags: { 'FINAL': false, 'END_FLOW_CONTROL': false }, | ||
flags: { END_FLOW_CONTROL: false }, | ||
stream: 10, | ||
@@ -139,10 +147,17 @@ length: 4, | ||
buffer: new Buffer('0004' + '09' + '00' + '0000000A' + '12345678', 'hex') | ||
}] | ||
}]; | ||
// Concatenate two buffer into a new buffer | ||
function concat(buffer1, buffer2) { | ||
var concatenated = new Buffer(buffer1.length + buffer2.length) | ||
buffer1.copy(concatenated) | ||
buffer2.copy(concatenated, buffer1.length) | ||
return concatenated | ||
function concat(buffers) { | ||
var size = 0; | ||
for (var i = 0; i < buffers.length; i++) { | ||
size += buffers[i].length; | ||
} | ||
var concatenated = new Buffer(size); | ||
for (var cursor = 0, j = 0; j < buffers.length; cursor += buffers[j].length, j++) { | ||
buffers[j].copy(concatenated, cursor); | ||
} | ||
return concatenated; | ||
} | ||
@@ -152,60 +167,65 @@ | ||
function shuffle_buffers(buffers) { | ||
var concatenated = new Buffer(0) | ||
for (var i = 0; i < buffers.length; i++) concatenated = concat(concatenated, buffers[i]) | ||
var concatenated = concat(buffers), output = [], written = 0; | ||
var output = [] | ||
var written = 0 | ||
while (written < concatenated.length) { | ||
var chunk_size = Math.min(concatenated.length - written, Math.ceil(Math.random()*20)) | ||
output.push(concatenated.slice(written, written + chunk_size)) | ||
written += chunk_size | ||
var chunk_size = Math.min(concatenated.length - written, Math.ceil(Math.random()*20)); | ||
output.push(concatenated.slice(written, written + chunk_size)); | ||
written += chunk_size; | ||
} | ||
return output | ||
return output; | ||
} | ||
describe('Framer', function() { | ||
describe('framer.js', function() { | ||
describe('Serializer', function() { | ||
describe('static method .commonHeader({ length, type, flags, stream })', function() { | ||
it('should return the appropriate 8 byte header buffer', function() { | ||
describe('static method .commonHeader({ type, flags, stream }, buffer_array)', function() { | ||
it('should add the appropriate 8 byte header buffer in front of the others', function() { | ||
for (var i = 0; i < test_frames.length; i++) { | ||
var test = test_frames[i] | ||
expect(Serializer.commonHeader(test.frame)).to.deep.equal(test.buffer.slice(0,8)) | ||
, buffers = [test.buffer.slice(8)] | ||
, header_buffer = test.buffer.slice(0,8); | ||
Serializer.commonHeader(test.frame, buffers); | ||
expect(buffers[0]).to.deep.equal(header_buffer); | ||
} | ||
}) | ||
}) | ||
}); | ||
}); | ||
Object.keys(frame_types).forEach(function(type) { | ||
var tests = test_frames.filter(function(test) { return test.frame.type === type }) | ||
var frame_shape = '{ ' + frame_types[type].join(', ') + ' }' | ||
describe('static method [\'' + type + '\'](' + frame_shape + ')', function() { | ||
it('should return a ' + type + ' type payload buffer', function() { | ||
var tests = test_frames.filter(function(test) { return test.frame.type === type }); | ||
var frame_shape = '{ ' + frame_types[type].join(', ') + ' }'; | ||
describe('static method .' + type + '(' + frame_shape + ', buffer_array)', function() { | ||
it('should push buffers to the array that make up a ' + type + ' type payload', function() { | ||
for (var i = 0; i < tests.length; i++) { | ||
var test = tests[i] | ||
expect(Serializer[type](test.frame)).to.deep.equal(test.buffer.slice(8)) | ||
, buffers = []; | ||
Serializer[type](test.frame, buffers); | ||
expect(concat(buffers)).to.deep.equal(test.buffer.slice(8)); | ||
} | ||
}) | ||
}) | ||
}) | ||
}); | ||
}); | ||
}); | ||
describe('transform stream', function() { | ||
it('should transform frame objects to appropriate buffers', function() { | ||
var stream = new Serializer() | ||
var stream = new Serializer(); | ||
for (var i = 0; i < test_frames.length; i++) { | ||
var test = test_frames[i] | ||
stream.write(test.frame) | ||
var chunk, buffer = new Buffer(0) | ||
while (chunk = stream.read()) buffer = concat(buffer, chunk) | ||
expect(buffer).to.be.deep.equal(test.buffer) | ||
var test = test_frames[i]; | ||
stream.write(test.frame); | ||
var chunk, buffer = new Buffer(0); | ||
while (chunk = stream.read()) { | ||
buffer = concat([buffer, chunk]); | ||
} | ||
expect(buffer).to.be.deep.equal(test.buffer); | ||
} | ||
}) | ||
}) | ||
}) | ||
}); | ||
}); | ||
}); | ||
describe('Deserializer', function() { | ||
describe('static method .commonHeader(header_buffer)', function() { | ||
it('should return the appropriate header object', function() { | ||
describe('static method .commonHeader(header_buffer, frame)', function() { | ||
it('should augment the frame object with these properties: { length, type, flags, stream })', function() { | ||
for (var i = 0; i < test_frames.length; i++) { | ||
var test = test_frames[i] | ||
expect(Deserializer.commonHeader(test.buffer.slice(0,8))).to.deep.equal({ | ||
var test = test_frames[i], frame = {}; | ||
Deserializer.commonHeader(test.buffer.slice(0,8), frame); | ||
expect(frame).to.deep.equal({ | ||
length: test.frame.length, | ||
@@ -215,66 +235,42 @@ type: test.frame.type, | ||
stream: test.frame.stream | ||
}) | ||
}); | ||
} | ||
}) | ||
}) | ||
}); | ||
}); | ||
Object.keys(frame_types).forEach(function(type) { | ||
var tests = test_frames.filter(function(test) { return test.frame.type === type }) | ||
var frame_shape = '{ ' + frame_types[type].join(', ') + ' }' | ||
describe('static method [\'' + type + '\'](payload_buffer)', function() { | ||
it('should return the parsed frame object with these properties: ' + frame_shape, function() { | ||
var tests = test_frames.filter(function(test) { return test.frame.type === type }); | ||
var frame_shape = '{ ' + frame_types[type].join(', ') + ' }'; | ||
describe('static method .' + type + '(payload_buffer, frame)', function() { | ||
it('should augment the frame object with these properties: ' + frame_shape, function() { | ||
for (var i = 0; i < tests.length; i++) { | ||
var test = tests[i] | ||
var parsed = Deserializer[type](test.buffer.slice(8)) | ||
parsed.length = test.frame.length | ||
parsed.type = test.frame.type | ||
parsed.flags = test.frame.flags | ||
parsed.stream = test.frame.stream | ||
expect(parsed).to.deep.equal(test.frame) | ||
var test = tests[i]; | ||
var frame = { | ||
length: test.frame.length, | ||
type: test.frame.type, | ||
flags: test.frame.flags, | ||
stream: test.frame.stream | ||
}; | ||
Deserializer[type](test.buffer.slice(8), frame); | ||
expect(frame).to.deep.equal(test.frame); | ||
} | ||
}) | ||
}) | ||
}) | ||
}); | ||
}); | ||
}); | ||
describe('transform stream', function() { | ||
it('should transform buffers to appropriate frame object', function() { | ||
var stream = new Deserializer() | ||
var stream = new Deserializer(); | ||
shuffle_buffers(test_frames.map(function(test) { return test.buffer })) | ||
.forEach(stream.write.bind(stream)) | ||
.forEach(stream.write.bind(stream)); | ||
for (var j = 0; j < test_frames.length; j++) { | ||
var parsed_frame = stream.read() | ||
parsed_frame.length = test_frames[j].frame.length | ||
expect(parsed_frame).to.be.deep.equal(test_frames[j].frame) | ||
var parsed_frame = stream.read(); | ||
parsed_frame.length = test_frames[j].frame.length; | ||
expect(parsed_frame).to.be.deep.equal(test_frames[j].frame); | ||
} | ||
}) | ||
}) | ||
}) | ||
describe('invariant', function() { | ||
describe('header === Deserializer.commonHeader(Serializer.commonHeader(header))', function() { | ||
it('should always be true for well formed header objects', function() { | ||
for (var i = 0; i < test_frames.length; i++) { | ||
var frame = test_frames[i].frame | ||
var header = { | ||
length: frame.length, | ||
type: frame.type, | ||
flags: frame.flags, | ||
stream: frame.stream | ||
} | ||
expect(Deserializer.commonHeader(Serializer.commonHeader(header))).to.deep.equal(header) | ||
} | ||
}) | ||
}) | ||
describe('buffer === Serializer.commonHeader(Deserializer.commonHeader(buffer))', function() { | ||
it('should always be true for well formed header buffers', function() { | ||
for (var i = 0; i < test_frames.length; i++) { | ||
var buffer = test_frames[i].buffer.slice(0,8) | ||
expect(Serializer.commonHeader(Deserializer.commonHeader(buffer))).to.deep.equal(buffer) | ||
} | ||
}) | ||
}) | ||
}) | ||
}) | ||
}); | ||
}); | ||
}); | ||
}); |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Misc. License Issues
License(Experimental) A package's licensing information has fine-grained problems.
Found 1 instance in 1 package
No License Found
License(Experimental) License information could not be found.
Found 1 instance in 1 package
504479
22
0
0
2020
39
0