michael@0: // The framer consists of two [Transform Stream][1] subclasses that operate in [object mode][2]: michael@0: // the Serializer and the Deserializer michael@0: // [1]: http://nodejs.org/api/stream.html#stream_class_stream_transform michael@0: // [2]: http://nodejs.org/api/stream.html#stream_new_stream_readable_options michael@0: var assert = require('assert'); michael@0: michael@0: var Transform = require('stream').Transform; michael@0: michael@0: exports.Serializer = Serializer; michael@0: exports.Deserializer = Deserializer; michael@0: michael@0: var logData = Boolean(process.env.HTTP2_LOG_DATA); michael@0: michael@0: var MAX_PAYLOAD_SIZE = 16383; michael@0: michael@0: // Serializer michael@0: // ---------- michael@0: // michael@0: // Frame Objects michael@0: // * * * * * * * --+--------------------------- michael@0: // | | michael@0: // v v Buffers michael@0: // [] -----> Payload Ser. --[buffers]--> Header Ser. --> * * * * michael@0: // empty adds payload adds header michael@0: // array buffers buffer michael@0: michael@0: function Serializer(log, sizeLimit) { michael@0: this._log = log.child({ component: 'serializer' }); michael@0: this._sizeLimit = sizeLimit || MAX_PAYLOAD_SIZE; michael@0: Transform.call(this, { objectMode: true }); michael@0: } michael@0: Serializer.prototype = Object.create(Transform.prototype, { constructor: { value: Serializer } }); michael@0: michael@0: // When there's an incoming frame object, it first generates the frame type specific part of the michael@0: // frame (payload), and then then adds the header part which holds fields that are common to all michael@0: // frame types (like the length of the payload). michael@0: Serializer.prototype._transform = function _transform(frame, encoding, done) { michael@0: this._log.trace({ frame: frame }, 'Outgoing frame'); michael@0: michael@0: assert(frame.type in Serializer, 'Unknown frame type: ' + frame.type); michael@0: michael@0: var buffers = []; michael@0: Serializer[frame.type](frame, buffers); michael@0: Serializer.commonHeader(frame, buffers); michael@0: michael@0: assert(buffers[0].readUInt16BE(0) <= this._sizeLimit, 'Frame too large!'); michael@0: michael@0: for (var i = 0; i < buffers.length; i++) { michael@0: if (logData) { michael@0: this._log.trace({ data: buffers[i] }, 'Outgoing data'); michael@0: } michael@0: this.push(buffers[i]); michael@0: } michael@0: michael@0: done(); michael@0: }; michael@0: michael@0: // Deserializer michael@0: // ------------ michael@0: // michael@0: // Buffers michael@0: // * * * * --------+------------------------- michael@0: // | | michael@0: // v v Frame Objects michael@0: // {} -----> Header Des. --{frame}--> Payload Des. --> * * * * * * * michael@0: // empty adds parsed adds parsed michael@0: // object header properties payload properties michael@0: michael@0: function Deserializer(log, sizeLimit, role) { michael@0: this._role = role; michael@0: this._log = log.child({ component: 'deserializer' }); michael@0: this._sizeLimit = sizeLimit || MAX_PAYLOAD_SIZE; michael@0: Transform.call(this, { objectMode: true }); michael@0: this._next(COMMON_HEADER_SIZE); michael@0: } michael@0: Deserializer.prototype = Object.create(Transform.prototype, { constructor: { value: Deserializer } }); michael@0: michael@0: // The Deserializer is stateful, and it's two main alternating states are: *waiting for header* and michael@0: // *waiting for payload*. The state is stored in the boolean property `_waitingForHeader`. michael@0: // michael@0: // When entering a new state, a `_buffer` is created that will hold the accumulated data (header or michael@0: // payload). The `_cursor` is used to track the progress. michael@0: Deserializer.prototype._next = function(size) { michael@0: this._cursor = 0; michael@0: this._buffer = new Buffer(size); michael@0: this._waitingForHeader = !this._waitingForHeader; michael@0: if (this._waitingForHeader) { michael@0: this._frame = {}; michael@0: } michael@0: }; michael@0: michael@0: // Parsing an incoming buffer is an iterative process because it can hold multiple frames if it's michael@0: // large enough. A `cursor` is used to track the progress in parsing the incoming `chunk`. michael@0: Deserializer.prototype._transform = function _transform(chunk, encoding, done) { michael@0: var cursor = 0; michael@0: michael@0: if (logData) { michael@0: this._log.trace({ data: chunk }, 'Incoming data'); michael@0: } michael@0: michael@0: while(cursor < chunk.length) { michael@0: // The content of an incoming buffer is first copied to `_buffer`. If it can't hold the full michael@0: // chunk, then only a part of it is copied. michael@0: var toCopy = Math.min(chunk.length - cursor, this._buffer.length - this._cursor); michael@0: chunk.copy(this._buffer, this._cursor, cursor, cursor + toCopy); michael@0: this._cursor += toCopy; michael@0: cursor += toCopy; michael@0: michael@0: // When `_buffer` is full, it's content gets parsed either as header or payload depending on michael@0: // the actual state. michael@0: michael@0: // If it's header then the parsed data is stored in a temporary variable and then the michael@0: // deserializer waits for the specified length payload. michael@0: if ((this._cursor === this._buffer.length) && this._waitingForHeader) { michael@0: var payloadSize = Deserializer.commonHeader(this._buffer, this._frame); michael@0: if (payloadSize <= this._sizeLimit) { michael@0: this._next(payloadSize); michael@0: } else { michael@0: this.emit('error', 'FRAME_SIZE_ERROR'); michael@0: return; michael@0: } michael@0: } michael@0: michael@0: // If it's payload then the the frame object is finalized and then gets pushed out. michael@0: // Unknown frame types are ignored. michael@0: // michael@0: // Note: If we just finished the parsing of a header and the payload length is 0, this branch michael@0: // will also run. michael@0: if ((this._cursor === this._buffer.length) && !this._waitingForHeader) { michael@0: if (this._frame.type) { michael@0: var error = Deserializer[this._frame.type](this._buffer, this._frame, this._role); michael@0: if (error) { michael@0: this._log.error('Incoming frame parsing error: ' + error); michael@0: this.emit('error', 'PROTOCOL_ERROR'); michael@0: } else { michael@0: this._log.trace({ frame: this._frame }, 'Incoming frame'); michael@0: this.push(this._frame); michael@0: } michael@0: } else { michael@0: this._log.error('Unknown type incoming frame'); michael@0: this.emit('error', 'PROTOCOL_ERROR'); michael@0: } michael@0: this._next(COMMON_HEADER_SIZE); michael@0: } michael@0: } michael@0: michael@0: done(); michael@0: }; michael@0: michael@0: // [Frame Header](http://tools.ietf.org/html/draft-ietf-httpbis-http2-10#section-4.1) michael@0: // -------------------------------------------------------------- michael@0: // michael@0: // HTTP/2.0 frames share a common base format consisting of an 8-byte header followed by 0 to 65535 michael@0: // bytes of data. michael@0: // michael@0: // Additional size limits can be set by specific application uses. HTTP limits the frame size to michael@0: // 16,383 octets. michael@0: // michael@0: // 0 1 2 3 michael@0: // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 michael@0: // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ michael@0: // | R | Length (14) | Type (8) | Flags (8) | michael@0: // +-+-+---------------------------+---------------+---------------+ michael@0: // |R| Stream Identifier (31) | michael@0: // +-+-------------------------------------------------------------+ michael@0: // | Frame Data (0...) ... michael@0: // +---------------------------------------------------------------+ michael@0: // michael@0: // The fields of the frame header are defined as: michael@0: // michael@0: // * R: michael@0: // A reserved 2-bit field. The semantics of these bits are undefined and the bits MUST remain michael@0: // unset (0) when sending and MUST be ignored when receiving. michael@0: // michael@0: // * Length: michael@0: // The length of the frame data expressed as an unsigned 14-bit integer. The 8 bytes of the frame michael@0: // header are not included in this value. michael@0: // michael@0: // * Type: michael@0: // The 8-bit type of the frame. The frame type determines how the remainder of the frame header michael@0: // and data are interpreted. Implementations MUST ignore unsupported and unrecognized frame types. michael@0: // michael@0: // * Flags: michael@0: // An 8-bit field reserved for frame-type specific boolean flags. michael@0: // michael@0: // Flags are assigned semantics specific to the indicated frame type. Flags that have no defined michael@0: // semantics for a particular frame type MUST be ignored, and MUST be left unset (0) when sending. michael@0: // michael@0: // * R: michael@0: // A reserved 1-bit field. The semantics of this bit are undefined and the bit MUST remain unset michael@0: // (0) when sending and MUST be ignored when receiving. michael@0: // michael@0: // * Stream Identifier: michael@0: // A 31-bit stream identifier. The value 0 is reserved for frames that are associated with the michael@0: // connection as a whole as opposed to an individual stream. michael@0: // michael@0: // The structure and content of the remaining frame data is dependent entirely on the frame type. michael@0: michael@0: var COMMON_HEADER_SIZE = 8; michael@0: michael@0: var frameTypes = []; michael@0: michael@0: var frameFlags = {}; michael@0: michael@0: var genericAttributes = ['type', 'flags', 'stream']; michael@0: michael@0: var typeSpecificAttributes = {}; michael@0: michael@0: Serializer.commonHeader = function writeCommonHeader(frame, buffers) { michael@0: var headerBuffer = new Buffer(COMMON_HEADER_SIZE); michael@0: michael@0: var size = 0; michael@0: for (var i = 0; i < buffers.length; i++) { michael@0: size += buffers[i].length; michael@0: } michael@0: headerBuffer.writeUInt16BE(size, 0); michael@0: michael@0: var typeId = frameTypes.indexOf(frame.type); // If we are here then the type is valid for sure michael@0: headerBuffer.writeUInt8(typeId, 2); michael@0: michael@0: var flagByte = 0; michael@0: for (var flag in frame.flags) { michael@0: var position = frameFlags[frame.type].indexOf(flag); michael@0: assert(position !== -1, 'Unknown flag for frame type ' + frame.type + ': ' + flag); michael@0: if (frame.flags[flag]) { michael@0: flagByte |= (1 << position); michael@0: } michael@0: } michael@0: headerBuffer.writeUInt8(flagByte, 3); michael@0: michael@0: assert((0 <= frame.stream) && (frame.stream < 0x7fffffff), frame.stream); michael@0: headerBuffer.writeUInt32BE(frame.stream || 0, 4); michael@0: michael@0: buffers.unshift(headerBuffer); michael@0: }; michael@0: michael@0: Deserializer.commonHeader = function readCommonHeader(buffer, frame) { michael@0: var length = buffer.readUInt16BE(0); michael@0: michael@0: frame.type = frameTypes[buffer.readUInt8(2)]; michael@0: michael@0: frame.flags = {}; michael@0: var flagByte = buffer.readUInt8(3); michael@0: var definedFlags = frameFlags[frame.type]; michael@0: for (var i = 0; i < definedFlags.length; i++) { michael@0: frame.flags[definedFlags[i]] = Boolean(flagByte & (1 << i)); michael@0: } michael@0: michael@0: frame.stream = buffer.readUInt32BE(4) & 0x7fffffff; michael@0: michael@0: return length; michael@0: }; michael@0: michael@0: // Frame types michael@0: // =========== michael@0: michael@0: // Every frame type is registered in the following places: michael@0: // michael@0: // * `frameTypes`: a register of frame type codes (used by `commonHeader()`) michael@0: // * `frameFlags`: a register of valid flags for frame types (used by `commonHeader()`) michael@0: // * `typeSpecificAttributes`: a register of frame specific frame object attributes (used by michael@0: // logging code and also serves as documentation for frame objects) michael@0: michael@0: // [DATA Frames](http://tools.ietf.org/html/draft-ietf-httpbis-http2-10#section-6.1) michael@0: // ------------------------------------------------------------ michael@0: // michael@0: // DATA frames (type=0x0) convey arbitrary, variable-length sequences of octets associated with a michael@0: // stream. michael@0: // michael@0: // The DATA frame defines the following flags: michael@0: // michael@0: // * END_STREAM (0x1): michael@0: // Bit 1 being set indicates that this frame is the last that the endpoint will send for the michael@0: // identified stream. michael@0: // * END_SEGMENT (0x2): michael@0: // Bit 2 being set indicates that this frame is the last for the current segment. Intermediaries michael@0: // MUST NOT coalesce frames across a segment boundary and MUST preserve segment boundaries when michael@0: // forwarding frames. michael@0: // * PAD_LOW (0x10): michael@0: // Bit 5 being set indicates that the Pad Low field is present. michael@0: // * PAD_HIGH (0x20): michael@0: // Bit 6 being set indicates that the Pad High field is present. This bit MUST NOT be set unless michael@0: // the PAD_LOW flag is also set. Endpoints that receive a frame with PAD_HIGH set and PAD_LOW michael@0: // cleared MUST treat this as a connection error of type PROTOCOL_ERROR. michael@0: michael@0: frameTypes[0x0] = 'DATA'; michael@0: michael@0: frameFlags.DATA = ['END_STREAM', 'END_SEGMENT', 'RESERVED4', 'RESERVED8', 'PAD_LOW', 'PAD_HIGH']; michael@0: michael@0: typeSpecificAttributes.DATA = ['data']; michael@0: michael@0: Serializer.DATA = function writeData(frame, buffers) { michael@0: buffers.push(frame.data); michael@0: }; michael@0: michael@0: Deserializer.DATA = function readData(buffer, frame) { michael@0: var dataOffset = 0; michael@0: var paddingLength = 0; michael@0: if (frame.flags.PAD_LOW) { michael@0: if (frame.flags.PAD_HIGH) { michael@0: paddingLength = (buffer.readUInt8(dataOffset) & 0xff) * 256; michael@0: dataOffset += 1; michael@0: } michael@0: paddingLength += (buffer.readUInt8(dataOffset) & 0xff); michael@0: dataOffset += 1; michael@0: } else if (frame.flags.PAD_HIGH) { michael@0: return 'DATA frame got PAD_HIGH without PAD_LOW'; michael@0: } michael@0: if (paddingLength) { michael@0: frame.data = buffer.slice(dataOffset, -1 * paddingLength); michael@0: } else { michael@0: frame.data = buffer.slice(dataOffset); michael@0: } michael@0: }; michael@0: michael@0: // [HEADERS](http://tools.ietf.org/html/draft-ietf-httpbis-http2-10#section-6.2) michael@0: // -------------------------------------------------------------- michael@0: // michael@0: // The HEADERS frame (type=0x1) allows the sender to create a stream. michael@0: // michael@0: // The HEADERS frame defines the following flags: michael@0: // michael@0: // * END_STREAM (0x1): michael@0: // Bit 1 being set indicates that this frame is the last that the endpoint will send for the michael@0: // identified stream. michael@0: // * END_SEGMENT (0x2): michael@0: // Bit 2 being set indicates that this frame is the last for the current segment. Intermediaries michael@0: // MUST NOT coalesce frames across a segment boundary and MUST preserve segment boundaries when michael@0: // forwarding frames. michael@0: // * END_HEADERS (0x4): michael@0: // The END_HEADERS bit indicates that this frame contains the entire payload necessary to provide michael@0: // a complete set of headers. michael@0: // * PRIORITY (0x8): michael@0: // Bit 4 being set indicates that the first four octets of this frame contain a single reserved michael@0: // bit and a 31-bit priority. michael@0: // * PAD_LOW (0x10): michael@0: // Bit 5 being set indicates that the Pad Low field is present. michael@0: // * PAD_HIGH (0x20): michael@0: // Bit 6 being set indicates that the Pad High field is present. This bit MUST NOT be set unless michael@0: // the PAD_LOW flag is also set. Endpoints that receive a frame with PAD_HIGH set and PAD_LOW michael@0: // cleared MUST treat this as a connection error of type PROTOCOL_ERROR. michael@0: michael@0: frameTypes[0x1] = 'HEADERS'; michael@0: michael@0: frameFlags.HEADERS = ['END_STREAM', 'END_SEGMENT', 'END_HEADERS', 'PRIORITY', 'PAD_LOW', 'PAD_HIGH']; michael@0: michael@0: typeSpecificAttributes.HEADERS = ['priority', 'headers', 'data']; michael@0: michael@0: // 0 1 2 3 michael@0: // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 michael@0: // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ michael@0: // |X| (Optional) Priority (31) | michael@0: // +-+-------------------------------------------------------------+ michael@0: // | Header Block (*) ... michael@0: // +---------------------------------------------------------------+ michael@0: // michael@0: // The payload of a HEADERS frame contains a Headers Block michael@0: michael@0: Serializer.HEADERS = function writeHeadersPriority(frame, buffers) { michael@0: if (frame.flags.PRIORITY) { michael@0: var buffer = new Buffer(4); michael@0: assert((0 <= frame.priority) && (frame.priority <= 0xffffffff), frame.priority); michael@0: buffer.writeUInt32BE(frame.priority, 0); michael@0: buffers.push(buffer); michael@0: } michael@0: buffers.push(frame.data); michael@0: }; michael@0: michael@0: Deserializer.HEADERS = function readHeadersPriority(buffer, frame) { michael@0: var dataOffset = 0; michael@0: var paddingLength = 0; michael@0: if (frame.flags.PAD_LOW) { michael@0: if (frame.flags.PAD_HIGH) { michael@0: paddingLength = (buffer.readUInt8(dataOffset) & 0xff) * 256; michael@0: dataOffset += 1; michael@0: } michael@0: paddingLength += (buffer.readUInt8(dataOffset) & 0xff); michael@0: dataOffset += 1; michael@0: } else if (frame.flags.PAD_HIGH) { michael@0: return 'HEADERS frame got PAD_HIGH without PAD_LOW'; michael@0: } michael@0: if (frame.flags.PRIORITY) { michael@0: frame.priority = buffer.readUInt32BE(dataOffset) & 0x7fffffff; michael@0: dataOffset += 4; michael@0: } michael@0: if (paddingLength) { michael@0: frame.data = buffer.slice(dataOffset, -1 * paddingLength); michael@0: } else { michael@0: frame.data = buffer.slice(dataOffset); michael@0: } michael@0: }; michael@0: michael@0: // [PRIORITY](http://tools.ietf.org/html/draft-ietf-httpbis-http2-10#section-6.3) michael@0: // ------------------------------------------------------- michael@0: // michael@0: // The PRIORITY frame (type=0x2) specifies the sender-advised priority of a stream. michael@0: // michael@0: // The PRIORITY frame does not define any flags. michael@0: michael@0: frameTypes[0x2] = 'PRIORITY'; michael@0: michael@0: frameFlags.PRIORITY = []; michael@0: michael@0: typeSpecificAttributes.PRIORITY = ['priority']; michael@0: michael@0: // 0 1 2 3 michael@0: // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 michael@0: // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ michael@0: // |X| Priority (31) | michael@0: // +-+-------------------------------------------------------------+ michael@0: // michael@0: // The payload of a PRIORITY frame contains a single reserved bit and a 31-bit priority. michael@0: michael@0: Serializer.PRIORITY = function writePriority(frame, buffers) { michael@0: var buffer = new Buffer(4); michael@0: buffer.writeUInt32BE(frame.priority, 0); michael@0: buffers.push(buffer); michael@0: }; michael@0: michael@0: Deserializer.PRIORITY = function readPriority(buffer, frame) { michael@0: frame.priority = buffer.readUInt32BE(0); michael@0: }; michael@0: michael@0: // [RST_STREAM](http://tools.ietf.org/html/draft-ietf-httpbis-http2-10#section-6.4) michael@0: // ----------------------------------------------------------- michael@0: // michael@0: // The RST_STREAM frame (type=0x3) allows for abnormal termination of a stream. michael@0: // michael@0: // No type-flags are defined. michael@0: michael@0: frameTypes[0x3] = 'RST_STREAM'; michael@0: michael@0: frameFlags.RST_STREAM = []; michael@0: michael@0: typeSpecificAttributes.RST_STREAM = ['error']; michael@0: michael@0: // 0 1 2 3 michael@0: // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 michael@0: // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ michael@0: // | Error Code (32) | michael@0: // +---------------------------------------------------------------+ michael@0: // michael@0: // The RST_STREAM frame contains a single unsigned, 32-bit integer identifying the error michael@0: // code (see Error Codes). The error code indicates why the stream is being terminated. michael@0: michael@0: Serializer.RST_STREAM = function writeRstStream(frame, buffers) { michael@0: var buffer = new Buffer(4); michael@0: var code = errorCodes.indexOf(frame.error); michael@0: assert((0 <= code) && (code <= 0xffffffff), code); michael@0: buffer.writeUInt32BE(code, 0); michael@0: buffers.push(buffer); michael@0: }; michael@0: michael@0: Deserializer.RST_STREAM = function readRstStream(buffer, frame) { michael@0: frame.error = errorCodes[buffer.readUInt32BE(0)]; michael@0: }; michael@0: michael@0: // [SETTINGS](http://tools.ietf.org/html/draft-ietf-httpbis-http2-10#section-6.5) michael@0: // ------------------------------------------------------- michael@0: // michael@0: // The SETTINGS frame (type=0x4) conveys configuration parameters that affect how endpoints michael@0: // communicate. michael@0: // michael@0: // The SETTINGS frame defines the following flag: michael@0: michael@0: // * ACK (0x1): michael@0: // Bit 1 being set indicates that this frame acknowledges receipt and application of the peer's michael@0: // SETTINGS frame. michael@0: frameTypes[0x4] = 'SETTINGS'; michael@0: michael@0: frameFlags.SETTINGS = ['ACK']; michael@0: michael@0: typeSpecificAttributes.SETTINGS = ['settings']; michael@0: michael@0: // The payload of a SETTINGS frame consists of zero or more settings. Each setting consists of an michael@0: // 8-bit identifier, and an unsigned 32-bit value. michael@0: // michael@0: // 0 1 2 3 michael@0: // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 michael@0: // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ michael@0: // | Identifier(8) | Value (32) | michael@0: // +-----------------+---------------------------------------------+ michael@0: // ...Value | michael@0: // +-----------------+ michael@0: // michael@0: // Each setting in a SETTINGS frame replaces the existing value for that setting. Settings are michael@0: // processed in the order in which they appear, and a receiver of a SETTINGS frame does not need to michael@0: // maintain any state other than the current value of settings. Therefore, the value of a setting michael@0: // is the last value that is seen by a receiver. This permits the inclusion of the same settings michael@0: // multiple times in the same SETTINGS frame, though doing so does nothing other than waste michael@0: // connection capacity. michael@0: michael@0: Serializer.SETTINGS = function writeSettings(frame, buffers) { michael@0: var settings = [], settingsLeft = Object.keys(frame.settings); michael@0: definedSettings.forEach(function(setting, id) { michael@0: if (setting.name in frame.settings) { michael@0: settingsLeft.splice(settingsLeft.indexOf(setting.name), 1); michael@0: var value = frame.settings[setting.name]; michael@0: settings.push({ id: id, value: setting.flag ? Boolean(value) : value }); michael@0: } michael@0: }); michael@0: assert(settingsLeft.length === 0, 'Unknown settings: ' + settingsLeft.join(', ')); michael@0: michael@0: var buffer = new Buffer(settings.length * 5); michael@0: for (var i = 0; i < settings.length; i++) { michael@0: buffer.writeUInt8(settings[i].id & 0xff, i*5); michael@0: buffer.writeUInt32BE(settings[i].value, i*5 + 1); michael@0: } michael@0: michael@0: buffers.push(buffer); michael@0: }; michael@0: michael@0: Deserializer.SETTINGS = function readSettings(buffer, frame, role) { michael@0: frame.settings = {}; michael@0: michael@0: if (buffer.length % 5 !== 0) { michael@0: return 'Invalid SETTINGS frame'; michael@0: } michael@0: for (var i = 0; i < buffer.length / 5; i++) { michael@0: var id = buffer.readUInt8(i*5) & 0xff; michael@0: var setting = definedSettings[id]; michael@0: if (setting) { michael@0: if (role == 'CLIENT' && setting.name == 'SETTINGS_ENABLE_PUSH') { michael@0: return 'SETTINGS frame on client got SETTINGS_ENABLE_PUSH'; michael@0: } michael@0: var value = buffer.readUInt32BE(i*5 + 1); michael@0: frame.settings[setting.name] = setting.flag ? Boolean(value & 0x1) : value; michael@0: } else { michael@0: /* Unknown setting, protocol error */ michael@0: return 'SETTINGS frame got unknown setting type'; michael@0: } michael@0: } michael@0: }; michael@0: michael@0: // The following settings are defined: michael@0: var definedSettings = []; michael@0: michael@0: // * SETTINGS_HEADER_TABLE_SIZE (1): michael@0: // Allows the sender to inform the remote endpoint of the size of the header compression table michael@0: // used to decode header blocks. michael@0: definedSettings[1] = { name: 'SETTINGS_HEADER_TABLE_SIZE', flag: false }; michael@0: michael@0: // * SETTINGS_ENABLE_PUSH (2): michael@0: // This setting can be use to disable server push. An endpoint MUST NOT send a PUSH_PROMISE frame michael@0: // if it receives this setting set to a value of 0. The default value is 1, which indicates that michael@0: // push is permitted. michael@0: definedSettings[2] = { name: 'SETTINGS_ENABLE_PUSH', flag: true }; michael@0: michael@0: // * SETTINGS_MAX_CONCURRENT_STREAMS (4): michael@0: // indicates the maximum number of concurrent streams that the sender will allow. michael@0: definedSettings[3] = { name: 'SETTINGS_MAX_CONCURRENT_STREAMS', flag: false }; michael@0: michael@0: // * SETTINGS_INITIAL_WINDOW_SIZE (7): michael@0: // indicates the sender's initial stream window size (in bytes) for new streams. michael@0: definedSettings[4] = { name: 'SETTINGS_INITIAL_WINDOW_SIZE', flag: false }; michael@0: michael@0: // [PUSH_PROMISE](http://tools.ietf.org/html/draft-ietf-httpbis-http2-10#section-6.6) michael@0: // --------------------------------------------------------------- michael@0: // michael@0: // The PUSH_PROMISE frame (type=0x5) is used to notify the peer endpoint in advance of streams the michael@0: // sender intends to initiate. michael@0: // michael@0: // The PUSH_PROMISE frame defines the following flags: michael@0: // michael@0: // * END_PUSH_PROMISE (0x4): michael@0: // The END_PUSH_PROMISE bit indicates that this frame contains the entire payload necessary to michael@0: // provide a complete set of headers. michael@0: michael@0: frameTypes[0x5] = 'PUSH_PROMISE'; michael@0: michael@0: frameFlags.PUSH_PROMISE = ['RESERVED1', 'RESERVED2', 'END_PUSH_PROMISE']; michael@0: michael@0: typeSpecificAttributes.PUSH_PROMISE = ['promised_stream', 'headers', 'data']; michael@0: michael@0: // 0 1 2 3 michael@0: // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 michael@0: // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ michael@0: // |X| Promised-Stream-ID (31) | michael@0: // +-+-------------------------------------------------------------+ michael@0: // | Header Block (*) ... michael@0: // +---------------------------------------------------------------+ michael@0: // michael@0: // The PUSH_PROMISE frame includes the unsigned 31-bit identifier of michael@0: // the stream the endpoint plans to create along with a minimal set of headers that provide michael@0: // additional context for the stream. michael@0: michael@0: Serializer.PUSH_PROMISE = function writePushPromise(frame, buffers) { michael@0: var buffer = new Buffer(4); michael@0: michael@0: var promised_stream = frame.promised_stream; michael@0: assert((0 <= promised_stream) && (promised_stream <= 0x7fffffff), promised_stream); michael@0: buffer.writeUInt32BE(promised_stream, 0); michael@0: michael@0: buffers.push(buffer); michael@0: buffers.push(frame.data); michael@0: }; michael@0: michael@0: Deserializer.PUSH_PROMISE = function readPushPromise(buffer, frame) { michael@0: frame.promised_stream = buffer.readUInt32BE(0) & 0x7fffffff; michael@0: frame.data = buffer.slice(4); michael@0: }; michael@0: michael@0: // [PING](http://tools.ietf.org/html/draft-ietf-httpbis-http2-10#section-6.7) michael@0: // ----------------------------------------------- michael@0: // michael@0: // The PING frame (type=0x6) is a mechanism for measuring a minimal round-trip time from the michael@0: // sender, as well as determining whether an idle connection is still functional. michael@0: // michael@0: // The PING frame defines one type-specific flag: michael@0: // michael@0: // * ACK (0x1): michael@0: // Bit 1 being set indicates that this PING frame is a PING response. michael@0: michael@0: frameTypes[0x6] = 'PING'; michael@0: michael@0: frameFlags.PING = ['ACK']; michael@0: michael@0: typeSpecificAttributes.PING = ['data']; michael@0: michael@0: // In addition to the frame header, PING frames MUST contain 8 additional octets of opaque data. michael@0: michael@0: Serializer.PING = function writePing(frame, buffers) { michael@0: buffers.push(frame.data); michael@0: }; michael@0: michael@0: Deserializer.PING = function readPing(buffer, frame) { michael@0: if (buffer.length !== 8) { michael@0: return 'Invalid size PING frame'; michael@0: } michael@0: frame.data = buffer; michael@0: }; michael@0: michael@0: // [GOAWAY](http://tools.ietf.org/html/draft-ietf-httpbis-http2-10#section-6.8) michael@0: // --------------------------------------------------- michael@0: // michael@0: // The GOAWAY frame (type=0x7) informs the remote peer to stop creating streams on this connection. michael@0: // michael@0: // The GOAWAY frame does not define any flags. michael@0: michael@0: frameTypes[0x7] = 'GOAWAY'; michael@0: michael@0: frameFlags.GOAWAY = []; michael@0: michael@0: typeSpecificAttributes.GOAWAY = ['last_stream', 'error']; michael@0: michael@0: // 0 1 2 3 michael@0: // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 michael@0: // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ michael@0: // |X| Last-Stream-ID (31) | michael@0: // +-+-------------------------------------------------------------+ michael@0: // | Error Code (32) | michael@0: // +---------------------------------------------------------------+ michael@0: // michael@0: // The last stream identifier in the GOAWAY frame contains the highest numbered stream identifier michael@0: // for which the sender of the GOAWAY frame has received frames on and might have taken some action michael@0: // on. michael@0: // michael@0: // The GOAWAY frame also contains a 32-bit error code (see Error Codes) that contains the reason for michael@0: // closing the connection. michael@0: michael@0: Serializer.GOAWAY = function writeGoaway(frame, buffers) { michael@0: var buffer = new Buffer(8); michael@0: michael@0: var last_stream = frame.last_stream; michael@0: assert((0 <= last_stream) && (last_stream <= 0x7fffffff), last_stream); michael@0: buffer.writeUInt32BE(last_stream, 0); michael@0: michael@0: var code = errorCodes.indexOf(frame.error); michael@0: assert((0 <= code) && (code <= 0xffffffff), code); michael@0: buffer.writeUInt32BE(code, 4); michael@0: michael@0: buffers.push(buffer); michael@0: }; michael@0: michael@0: Deserializer.GOAWAY = function readGoaway(buffer, frame) { michael@0: frame.last_stream = buffer.readUInt32BE(0) & 0x7fffffff; michael@0: frame.error = errorCodes[buffer.readUInt32BE(4)]; michael@0: }; michael@0: michael@0: // [WINDOW_UPDATE](http://tools.ietf.org/html/draft-ietf-httpbis-http2-10#section-6.9) michael@0: // ----------------------------------------------------------------- michael@0: // michael@0: // The WINDOW_UPDATE frame (type=0x8) is used to implement flow control. michael@0: // michael@0: // The WINDOW_UPDATE frame does not define any flags. michael@0: michael@0: frameTypes[0x8] = 'WINDOW_UPDATE'; michael@0: michael@0: frameFlags.WINDOW_UPDATE = []; michael@0: michael@0: typeSpecificAttributes.WINDOW_UPDATE = ['window_size']; michael@0: michael@0: // The payload of a WINDOW_UPDATE frame is a 32-bit value indicating the additional number of bytes michael@0: // that the sender can transmit in addition to the existing flow control window. The legal range michael@0: // for this field is 1 to 2^31 - 1 (0x7fffffff) bytes; the most significant bit of this value is michael@0: // reserved. michael@0: michael@0: Serializer.WINDOW_UPDATE = function writeWindowUpdate(frame, buffers) { michael@0: var buffer = new Buffer(4); michael@0: michael@0: var window_size = frame.window_size; michael@0: assert((0 <= window_size) && (window_size <= 0x7fffffff), window_size); michael@0: buffer.writeUInt32BE(window_size, 0); michael@0: michael@0: buffers.push(buffer); michael@0: }; michael@0: michael@0: Deserializer.WINDOW_UPDATE = function readWindowUpdate(buffer, frame) { michael@0: frame.window_size = buffer.readUInt32BE(0) & 0x7fffffff; michael@0: }; michael@0: michael@0: // [CONTINUATION](http://tools.ietf.org/html/draft-ietf-httpbis-http2-10#section-6.10) michael@0: // ------------------------------------------------------------ michael@0: // michael@0: // The CONTINUATION frame (type=0xA) is used to continue a sequence of header block fragments. michael@0: // michael@0: // The CONTINUATION frame defines the following flag: michael@0: // michael@0: // * END_HEADERS (0x4): michael@0: // The END_HEADERS bit indicates that this frame ends the sequence of header block fragments michael@0: // necessary to provide a complete set of headers. michael@0: // * PAD_LOW (0x10): michael@0: // Bit 5 being set indicates that the Pad Low field is present. michael@0: // * PAD_HIGH (0x20): michael@0: // Bit 6 being set indicates that the Pad High field is present. This bit MUST NOT be set unless michael@0: // the PAD_LOW flag is also set. Endpoints that receive a frame with PAD_HIGH set and PAD_LOW michael@0: // cleared MUST treat this as a connection error of type PROTOCOL_ERROR. michael@0: michael@0: frameTypes[0x9] = 'CONTINUATION'; michael@0: michael@0: frameFlags.CONTINUATION = ['RESERVED1', 'RESERVED2', 'END_HEADERS', 'RESERVED8', 'PAD_LOW', 'PAD_HIGH']; michael@0: michael@0: typeSpecificAttributes.CONTINUATION = ['headers', 'data']; michael@0: michael@0: Serializer.CONTINUATION = function writeContinuation(frame, buffers) { michael@0: buffers.push(frame.data); michael@0: }; michael@0: michael@0: Deserializer.CONTINUATION = function readContinuation(buffer, frame) { michael@0: var dataOffset = 0; michael@0: var paddingLength = 0; michael@0: if (frame.flags.PAD_LOW) { michael@0: if (frame.flags.PAD_HIGH) { michael@0: paddingLength = (buffer.readUInt8(dataOffset) & 0xff) * 256; michael@0: dataOffset += 1; michael@0: } michael@0: paddingLength += (buffer.readUInt8(dataOffset) & 0xff); michael@0: dataOffset += 1; michael@0: } else if (frame.flags.PAD_HIGH) { michael@0: return 'CONTINUATION frame got PAD_HIGH without PAD_LOW'; michael@0: } michael@0: if (paddingLength) { michael@0: frame.data = buffer.slice(dataOffset, -1 * paddingLength); michael@0: } else { michael@0: frame.data = buffer.slice(dataOffset); michael@0: } michael@0: }; michael@0: michael@0: // [Error Codes](http://tools.ietf.org/html/draft-ietf-httpbis-http2-10#section-7) michael@0: // ------------------------------------------------------------ michael@0: michael@0: var errorCodes = [ michael@0: 'NO_ERROR', michael@0: 'PROTOCOL_ERROR', michael@0: 'INTERNAL_ERROR', michael@0: 'FLOW_CONTROL_ERROR', michael@0: 'SETTINGS_TIMEOUT', michael@0: 'STREAM_CLOSED', michael@0: 'FRAME_SIZE_ERROR', michael@0: 'REFUSED_STREAM', michael@0: 'CANCEL', michael@0: 'COMPRESSION_ERROR', michael@0: 'CONNECT_ERROR' michael@0: ]; michael@0: errorCodes[420] = 'ENHANCE_YOUR_CALM'; michael@0: michael@0: // Logging michael@0: // ------- michael@0: michael@0: // [Bunyan serializers](https://github.com/trentm/node-bunyan#serializers) to improve logging output michael@0: // for debug messages emitted in this component. michael@0: exports.serializers = {}; michael@0: michael@0: // * `frame` serializer: it transforms data attributes from Buffers to hex strings and filters out michael@0: // flags that are not present. michael@0: var frameCounter = 0; michael@0: exports.serializers.frame = function(frame) { michael@0: if (!frame) { michael@0: return null; michael@0: } michael@0: michael@0: if ('id' in frame) { michael@0: return frame.id; michael@0: } michael@0: michael@0: frame.id = frameCounter; michael@0: frameCounter += 1; michael@0: michael@0: var logEntry = { id: frame.id }; michael@0: genericAttributes.concat(typeSpecificAttributes[frame.type]).forEach(function(name) { michael@0: logEntry[name] = frame[name]; michael@0: }); michael@0: michael@0: if (frame.data instanceof Buffer) { michael@0: if (logEntry.data.length > 50) { michael@0: logEntry.data = frame.data.slice(0, 47).toString('hex') + '...'; michael@0: } else { michael@0: logEntry.data = frame.data.toString('hex'); michael@0: } michael@0: michael@0: if (!('length' in logEntry)) { michael@0: logEntry.length = frame.data.length; michael@0: } michael@0: } michael@0: michael@0: if (frame.promised_stream instanceof Object) { michael@0: logEntry.promised_stream = 'stream-' + frame.promised_stream.id; michael@0: } michael@0: michael@0: logEntry.flags = Object.keys(frame.flags || {}).filter(function(name) { michael@0: return frame.flags[name] === true; michael@0: }); michael@0: michael@0: return logEntry; michael@0: }; michael@0: michael@0: // * `data` serializer: it simply transforms a buffer to a hex string. michael@0: exports.serializers.data = function(data) { michael@0: return data.toString('hex'); michael@0: };