| 'use strict' |
| |
| var parser = exports |
| |
| var transport = require('../../../spdy-transport') |
| var base = transport.protocol.base |
| var utils = base.utils |
| var constants = require('./').constants |
| |
| var assert = require('assert') |
| var util = require('util') |
| |
| function Parser (options) { |
| base.Parser.call(this, options) |
| |
| this.isServer = options.isServer |
| |
| this.waiting = constants.PREFACE_SIZE |
| this.state = 'preface' |
| this.pendingHeader = null |
| |
| // Header Block queue |
| this._lastHeaderBlock = null |
| this.maxFrameSize = constants.INITIAL_MAX_FRAME_SIZE |
| this.maxHeaderListSize = constants.DEFAULT_MAX_HEADER_LIST_SIZE |
| } |
| util.inherits(Parser, base.Parser) |
| |
| parser.create = function create (options) { |
| return new Parser(options) |
| } |
| |
| Parser.prototype.setMaxFrameSize = function setMaxFrameSize (size) { |
| this.maxFrameSize = size |
| } |
| |
| Parser.prototype.setMaxHeaderListSize = function setMaxHeaderListSize (size) { |
| this.maxHeaderListSize = size |
| } |
| |
| // Only for testing |
| Parser.prototype.skipPreface = function skipPreface () { |
| // Just some number bigger than 3.1, doesn't really matter for HTTP2 |
| this.setVersion(4) |
| |
| // Parse frame header! |
| this.state = 'frame-head' |
| this.waiting = constants.FRAME_HEADER_SIZE |
| } |
| |
| Parser.prototype.execute = function execute (buffer, callback) { |
| if (this.state === 'preface') { return this.onPreface(buffer, callback) } |
| |
| if (this.state === 'frame-head') { |
| return this.onFrameHead(buffer, callback) |
| } |
| |
| assert(this.state === 'frame-body' && this.pendingHeader !== null) |
| |
| var self = this |
| var header = this.pendingHeader |
| this.pendingHeader = null |
| |
| this.onFrameBody(header, buffer, function (err, frame) { |
| if (err) { |
| return callback(err) |
| } |
| |
| self.state = 'frame-head' |
| self.partial = false |
| self.waiting = constants.FRAME_HEADER_SIZE |
| callback(null, frame) |
| }) |
| } |
| |
| Parser.prototype.executePartial = function executePartial (buffer, callback) { |
| var header = this.pendingHeader |
| |
| assert.strictEqual(header.flags & constants.flags.PADDED, 0) |
| |
| if (this.window) { this.window.recv.update(-buffer.size) } |
| |
| callback(null, { |
| type: 'DATA', |
| id: header.id, |
| |
| // Partial DATA can't be FIN |
| fin: false, |
| data: buffer.take(buffer.size) |
| }) |
| } |
| |
| Parser.prototype.onPreface = function onPreface (buffer, callback) { |
| if (buffer.take(buffer.size).toString() !== constants.PREFACE) { |
| return callback(this.error(constants.error.PROTOCOL_ERROR, |
| 'Invalid preface')) |
| } |
| |
| this.skipPreface() |
| callback(null, null) |
| } |
| |
| Parser.prototype.onFrameHead = function onFrameHead (buffer, callback) { |
| var header = { |
| length: buffer.readUInt24BE(), |
| control: true, |
| type: buffer.readUInt8(), |
| flags: buffer.readUInt8(), |
| id: buffer.readUInt32BE() & 0x7fffffff |
| } |
| |
| if (header.length > this.maxFrameSize) { |
| return callback(this.error(constants.error.FRAME_SIZE_ERROR, |
| 'Frame length OOB')) |
| } |
| |
| header.control = header.type !== constants.frameType.DATA |
| |
| this.state = 'frame-body' |
| this.pendingHeader = header |
| this.waiting = header.length |
| this.partial = !header.control |
| |
| // TODO(indutny): eventually support partial padded DATA |
| if (this.partial) { |
| this.partial = (header.flags & constants.flags.PADDED) === 0 |
| } |
| |
| callback(null, null) |
| } |
| |
| Parser.prototype.onFrameBody = function onFrameBody (header, buffer, callback) { |
| var frameType = constants.frameType |
| |
| if (header.type === frameType.DATA) { |
| this.onDataFrame(header, buffer, callback) |
| } else if (header.type === frameType.HEADERS) { |
| this.onHeadersFrame(header, buffer, callback) |
| } else if (header.type === frameType.CONTINUATION) { |
| this.onContinuationFrame(header, buffer, callback) |
| } else if (header.type === frameType.WINDOW_UPDATE) { |
| this.onWindowUpdateFrame(header, buffer, callback) |
| } else if (header.type === frameType.RST_STREAM) { |
| this.onRSTFrame(header, buffer, callback) |
| } else if (header.type === frameType.SETTINGS) { |
| this.onSettingsFrame(header, buffer, callback) |
| } else if (header.type === frameType.PUSH_PROMISE) { |
| this.onPushPromiseFrame(header, buffer, callback) |
| } else if (header.type === frameType.PING) { |
| this.onPingFrame(header, buffer, callback) |
| } else if (header.type === frameType.GOAWAY) { |
| this.onGoawayFrame(header, buffer, callback) |
| } else if (header.type === frameType.PRIORITY) { |
| this.onPriorityFrame(header, buffer, callback) |
| } else if (header.type === frameType.X_FORWARDED_FOR) { |
| this.onXForwardedFrame(header, buffer, callback) |
| } else { |
| this.onUnknownFrame(header, buffer, callback) |
| } |
| } |
| |
| Parser.prototype.onUnknownFrame = function onUnknownFrame (header, buffer, callback) { |
| if (this._lastHeaderBlock !== null) { |
| callback(this.error(constants.error.PROTOCOL_ERROR, |
| 'Received unknown frame in the middle of a header block')) |
| return |
| } |
| callback(null, { type: 'unknown: ' + header.type }) |
| } |
| |
| Parser.prototype.unpadData = function unpadData (header, body, callback) { |
| var isPadded = (header.flags & constants.flags.PADDED) !== 0 |
| |
| if (!isPadded) { return callback(null, body) } |
| |
| if (!body.has(1)) { |
| return callback(this.error(constants.error.FRAME_SIZE_ERROR, |
| 'Not enough space for padding')) |
| } |
| |
| var pad = body.readUInt8() |
| if (!body.has(pad)) { |
| return callback(this.error(constants.error.PROTOCOL_ERROR, |
| 'Invalid padding size')) |
| } |
| |
| var contents = body.clone(body.size - pad) |
| body.skip(body.size) |
| callback(null, contents) |
| } |
| |
| Parser.prototype.onDataFrame = function onDataFrame (header, body, callback) { |
| var isEndStream = (header.flags & constants.flags.END_STREAM) !== 0 |
| |
| if (header.id === 0) { |
| return callback(this.error(constants.error.PROTOCOL_ERROR, |
| 'Received DATA frame with stream=0')) |
| } |
| |
| // Count received bytes |
| if (this.window) { |
| this.window.recv.update(-body.size) |
| } |
| |
| this.unpadData(header, body, function (err, data) { |
| if (err) { |
| return callback(err) |
| } |
| |
| callback(null, { |
| type: 'DATA', |
| id: header.id, |
| fin: isEndStream, |
| data: data.take(data.size) |
| }) |
| }) |
| } |
| |
| Parser.prototype.initHeaderBlock = function initHeaderBlock (header, |
| frame, |
| block, |
| callback) { |
| if (this._lastHeaderBlock) { |
| return callback(this.error(constants.error.PROTOCOL_ERROR, |
| 'Duplicate Stream ID')) |
| } |
| |
| this._lastHeaderBlock = { |
| id: header.id, |
| frame: frame, |
| queue: [], |
| size: 0 |
| } |
| |
| this.queueHeaderBlock(header, block, callback) |
| } |
| |
| Parser.prototype.queueHeaderBlock = function queueHeaderBlock (header, |
| block, |
| callback) { |
| var self = this |
| var item = this._lastHeaderBlock |
| if (!this._lastHeaderBlock || item.id !== header.id) { |
| return callback(this.error(constants.error.PROTOCOL_ERROR, |
| 'No matching stream for continuation')) |
| } |
| |
| var fin = (header.flags & constants.flags.END_HEADERS) !== 0 |
| |
| var chunks = block.toChunks() |
| for (var i = 0; i < chunks.length; i++) { |
| var chunk = chunks[i] |
| item.queue.push(chunk) |
| item.size += chunk.length |
| } |
| |
| if (item.size >= self.maxHeaderListSize) { |
| return callback(this.error(constants.error.PROTOCOL_ERROR, |
| 'Compressed header list is too large')) |
| } |
| |
| if (!fin) { return callback(null, null) } |
| this._lastHeaderBlock = null |
| |
| this.decompress.write(item.queue, function (err, chunks) { |
| if (err) { |
| return callback(self.error(constants.error.COMPRESSION_ERROR, |
| err.message)) |
| } |
| |
| var headers = {} |
| var size = 0 |
| for (var i = 0; i < chunks.length; i++) { |
| var header = chunks[i] |
| |
| size += header.name.length + header.value.length + 32 |
| if (size >= self.maxHeaderListSize) { |
| return callback(self.error(constants.error.PROTOCOL_ERROR, |
| 'Header list is too large')) |
| } |
| |
| if (/[A-Z]/.test(header.name)) { |
| return callback(self.error(constants.error.PROTOCOL_ERROR, |
| 'Header name must be lowercase')) |
| } |
| |
| utils.addHeaderLine(header.name, header.value, headers) |
| } |
| |
| item.frame.headers = headers |
| item.frame.path = headers[':path'] |
| |
| callback(null, item.frame) |
| }) |
| } |
| |
| Parser.prototype.onHeadersFrame = function onHeadersFrame (header, |
| body, |
| callback) { |
| var self = this |
| |
| if (header.id === 0) { |
| return callback(this.error(constants.error.PROTOCOL_ERROR, |
| 'Invalid stream id for HEADERS')) |
| } |
| |
| this.unpadData(header, body, function (err, data) { |
| if (err) { return callback(err) } |
| |
| var isPriority = (header.flags & constants.flags.PRIORITY) !== 0 |
| if (!data.has(isPriority ? 5 : 0)) { |
| return callback(self.error(constants.error.FRAME_SIZE_ERROR, |
| 'Not enough data for HEADERS')) |
| } |
| |
| var exclusive = false |
| var dependency = 0 |
| var weight = constants.DEFAULT_WEIGHT |
| if (isPriority) { |
| dependency = data.readUInt32BE() |
| exclusive = (dependency & 0x80000000) !== 0 |
| dependency &= 0x7fffffff |
| |
| // Weight's range is [1, 256] |
| weight = data.readUInt8() + 1 |
| } |
| |
| if (dependency === header.id) { |
| return callback(self.error(constants.error.PROTOCOL_ERROR, |
| 'Stream can\'t dependend on itself')) |
| } |
| |
| var streamInfo = { |
| type: 'HEADERS', |
| id: header.id, |
| priority: { |
| parent: dependency, |
| exclusive: exclusive, |
| weight: weight |
| }, |
| fin: (header.flags & constants.flags.END_STREAM) !== 0, |
| writable: true, |
| headers: null, |
| path: null |
| } |
| |
| self.initHeaderBlock(header, streamInfo, data, callback) |
| }) |
| } |
| |
| Parser.prototype.onContinuationFrame = function onContinuationFrame (header, |
| body, |
| callback) { |
| this.queueHeaderBlock(header, body, callback) |
| } |
| |
| Parser.prototype.onRSTFrame = function onRSTFrame (header, body, callback) { |
| if (body.size !== 4) { |
| return callback(this.error(constants.error.FRAME_SIZE_ERROR, |
| 'RST_STREAM length not 4')) |
| } |
| |
| if (header.id === 0) { |
| return callback(this.error(constants.error.PROTOCOL_ERROR, |
| 'Invalid stream id for RST_STREAM')) |
| } |
| |
| callback(null, { |
| type: 'RST', |
| id: header.id, |
| code: constants.errorByCode[body.readUInt32BE()] |
| }) |
| } |
| |
| Parser.prototype._validateSettings = function _validateSettings (settings) { |
| if (settings['enable_push'] !== undefined && |
| settings['enable_push'] !== 0 && |
| settings['enable_push'] !== 1) { |
| return this.error(constants.error.PROTOCOL_ERROR, |
| 'SETTINGS_ENABLE_PUSH must be 0 or 1') |
| } |
| |
| if (settings['initial_window_size'] !== undefined && |
| (settings['initial_window_size'] > constants.MAX_INITIAL_WINDOW_SIZE || |
| settings['initial_window_size'] < 0)) { |
| return this.error(constants.error.FLOW_CONTROL_ERROR, |
| 'SETTINGS_INITIAL_WINDOW_SIZE is OOB') |
| } |
| |
| if (settings['max_frame_size'] !== undefined && |
| (settings['max_frame_size'] > constants.ABSOLUTE_MAX_FRAME_SIZE || |
| settings['max_frame_size'] < constants.INITIAL_MAX_FRAME_SIZE)) { |
| return this.error(constants.error.PROTOCOL_ERROR, |
| 'SETTINGS_MAX_FRAME_SIZE is OOB') |
| } |
| |
| return undefined |
| } |
| |
| Parser.prototype.onSettingsFrame = function onSettingsFrame (header, |
| body, |
| callback) { |
| if (header.id !== 0) { |
| return callback(this.error(constants.error.PROTOCOL_ERROR, |
| 'Invalid stream id for SETTINGS')) |
| } |
| |
| var isAck = (header.flags & constants.flags.ACK) !== 0 |
| if (isAck && body.size !== 0) { |
| return callback(this.error(constants.error.FRAME_SIZE_ERROR, |
| 'SETTINGS with ACK and non-zero length')) |
| } |
| |
| if (isAck) { |
| return callback(null, { type: 'ACK_SETTINGS' }) |
| } |
| |
| if (body.size % 6 !== 0) { |
| return callback(this.error(constants.error.FRAME_SIZE_ERROR, |
| 'SETTINGS length not multiple of 6')) |
| } |
| |
| var settings = {} |
| while (!body.isEmpty()) { |
| var id = body.readUInt16BE() |
| var value = body.readUInt32BE() |
| var name = constants.settingsIndex[id] |
| |
| if (name) { |
| settings[name] = value |
| } |
| } |
| |
| var err = this._validateSettings(settings) |
| if (err !== undefined) { |
| return callback(err) |
| } |
| |
| callback(null, { |
| type: 'SETTINGS', |
| settings: settings |
| }) |
| } |
| |
| Parser.prototype.onPushPromiseFrame = function onPushPromiseFrame (header, |
| body, |
| callback) { |
| if (header.id === 0) { |
| return callback(this.error(constants.error.PROTOCOL_ERROR, |
| 'Invalid stream id for PUSH_PROMISE')) |
| } |
| |
| var self = this |
| this.unpadData(header, body, function (err, data) { |
| if (err) { |
| return callback(err) |
| } |
| |
| if (!data.has(4)) { |
| return callback(self.error(constants.error.FRAME_SIZE_ERROR, |
| 'PUSH_PROMISE length less than 4')) |
| } |
| |
| var streamInfo = { |
| type: 'PUSH_PROMISE', |
| id: header.id, |
| fin: false, |
| promisedId: data.readUInt32BE() & 0x7fffffff, |
| headers: null, |
| path: null |
| } |
| |
| self.initHeaderBlock(header, streamInfo, data, callback) |
| }) |
| } |
| |
| Parser.prototype.onPingFrame = function onPingFrame (header, body, callback) { |
| if (body.size !== 8) { |
| return callback(this.error(constants.error.FRAME_SIZE_ERROR, |
| 'PING length != 8')) |
| } |
| |
| if (header.id !== 0) { |
| return callback(this.error(constants.error.PROTOCOL_ERROR, |
| 'Invalid stream id for PING')) |
| } |
| |
| var ack = (header.flags & constants.flags.ACK) !== 0 |
| callback(null, { type: 'PING', opaque: body.take(body.size), ack: ack }) |
| } |
| |
| Parser.prototype.onGoawayFrame = function onGoawayFrame (header, |
| body, |
| callback) { |
| if (!body.has(8)) { |
| return callback(this.error(constants.error.FRAME_SIZE_ERROR, |
| 'GOAWAY length < 8')) |
| } |
| |
| if (header.id !== 0) { |
| return callback(this.error(constants.error.PROTOCOL_ERROR, |
| 'Invalid stream id for GOAWAY')) |
| } |
| |
| var frame = { |
| type: 'GOAWAY', |
| lastId: body.readUInt32BE(), |
| code: constants.goawayByCode[body.readUInt32BE()] |
| } |
| |
| if (body.size !== 0) { frame.debug = body.take(body.size) } |
| |
| callback(null, frame) |
| } |
| |
| Parser.prototype.onPriorityFrame = function onPriorityFrame (header, |
| body, |
| callback) { |
| if (body.size !== 5) { |
| return callback(this.error(constants.error.FRAME_SIZE_ERROR, |
| 'PRIORITY length != 5')) |
| } |
| |
| if (header.id === 0) { |
| return callback(this.error(constants.error.PROTOCOL_ERROR, |
| 'Invalid stream id for PRIORITY')) |
| } |
| |
| var dependency = body.readUInt32BE() |
| |
| // Again the range is from 1 to 256 |
| var weight = body.readUInt8() + 1 |
| |
| if (dependency === header.id) { |
| return callback(this.error(constants.error.PROTOCOL_ERROR, |
| 'Stream can\'t dependend on itself')) |
| } |
| |
| callback(null, { |
| type: 'PRIORITY', |
| id: header.id, |
| priority: { |
| exclusive: (dependency & 0x80000000) !== 0, |
| parent: dependency & 0x7fffffff, |
| weight: weight |
| } |
| }) |
| } |
| |
| Parser.prototype.onWindowUpdateFrame = function onWindowUpdateFrame (header, |
| body, |
| callback) { |
| if (body.size !== 4) { |
| return callback(this.error(constants.error.FRAME_SIZE_ERROR, |
| 'WINDOW_UPDATE length != 4')) |
| } |
| |
| var delta = body.readInt32BE() |
| if (delta === 0) { |
| return callback(this.error(constants.error.PROTOCOL_ERROR, |
| 'WINDOW_UPDATE delta == 0')) |
| } |
| |
| callback(null, { |
| type: 'WINDOW_UPDATE', |
| id: header.id, |
| delta: delta |
| }) |
| } |
| |
| Parser.prototype.onXForwardedFrame = function onXForwardedFrame (header, |
| body, |
| callback) { |
| callback(null, { |
| type: 'X_FORWARDED_FOR', |
| host: body.take(body.size).toString() |
| }) |
| } |