579 lines
15 KiB
JavaScript
Executable file
579 lines
15 KiB
JavaScript
Executable file
'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()
|
|
})
|
|
}
|