'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')
|
var OffsetBuffer = require('obuf')
|
|
function Parser (options) {
|
base.Parser.call(this, options)
|
|
this.isServer = options.isServer
|
this.waiting = constants.FRAME_HEADER_SIZE
|
this.state = 'frame-head'
|
this.pendingHeader = null
|
}
|
util.inherits(Parser, base.Parser)
|
|
parser.create = function create (options) {
|
return new Parser(options)
|
}
|
|
Parser.prototype.setMaxFrameSize = function setMaxFrameSize (size) {
|
// http2-only
|
}
|
|
Parser.prototype.setMaxHeaderListSize = function setMaxHeaderListSize (size) {
|
// http2-only
|
}
|
|
// Only for testing
|
Parser.prototype.skipPreface = function skipPreface () {
|
}
|
|
Parser.prototype.execute = function execute (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.waiting = constants.FRAME_HEADER_SIZE
|
self.partial = false
|
callback(null, frame)
|
})
|
}
|
|
Parser.prototype.executePartial = function executePartial (buffer, callback) {
|
var header = this.pendingHeader
|
|
if (this.window) {
|
this.window.recv.update(-buffer.size)
|
}
|
|
// DATA frame
|
callback(null, {
|
type: 'DATA',
|
id: header.id,
|
|
// Partial DATA can't be FIN
|
fin: false,
|
data: buffer.take(buffer.size)
|
})
|
}
|
|
Parser.prototype.onFrameHead = function onFrameHead (buffer, callback) {
|
var header = {
|
control: (buffer.peekUInt8() & 0x80) === 0x80,
|
version: null,
|
type: null,
|
id: null,
|
flags: null,
|
length: null
|
}
|
|
if (header.control) {
|
header.version = buffer.readUInt16BE() & 0x7fff
|
header.type = buffer.readUInt16BE()
|
} else {
|
header.id = buffer.readUInt32BE(0) & 0x7fffffff
|
}
|
header.flags = buffer.readUInt8()
|
header.length = buffer.readUInt24BE()
|
|
if (this.version === null && header.control) {
|
// TODO(indutny): do ProtocolError here and in the rest of errors
|
if (header.version !== 2 && header.version !== 3) {
|
return callback(new Error('Unsupported SPDY version: ' + header.version))
|
}
|
this.setVersion(header.version)
|
}
|
|
this.state = 'frame-body'
|
this.waiting = header.length
|
this.pendingHeader = header
|
this.partial = !header.control
|
|
callback(null, null)
|
}
|
|
Parser.prototype.onFrameBody = function onFrameBody (header, buffer, callback) {
|
// Data frame
|
if (!header.control) {
|
// Count received bytes
|
if (this.window) {
|
this.window.recv.update(-buffer.size)
|
}
|
|
// No support for compressed DATA
|
if ((header.flags & constants.flags.FLAG_COMPRESSED) !== 0) {
|
return callback(new Error('DATA compression not supported'))
|
}
|
|
if (header.id === 0) {
|
return callback(this.error(constants.error.PROTOCOL_ERROR,
|
'Invalid stream id for DATA'))
|
}
|
|
return callback(null, {
|
type: 'DATA',
|
id: header.id,
|
fin: (header.flags & constants.flags.FLAG_FIN) !== 0,
|
data: buffer.take(buffer.size)
|
})
|
}
|
|
if (header.type === 0x01 || header.type === 0x02) { // SYN_STREAM or SYN_REPLY
|
this.onSynHeadFrame(header.type, header.flags, buffer, callback)
|
} else if (header.type === 0x03) { // RST_STREAM
|
this.onRSTFrame(buffer, callback)
|
} else if (header.type === 0x04) { // SETTINGS
|
this.onSettingsFrame(buffer, callback)
|
} else if (header.type === 0x05) {
|
callback(null, { type: 'NOOP' })
|
} else if (header.type === 0x06) { // PING
|
this.onPingFrame(buffer, callback)
|
} else if (header.type === 0x07) { // GOAWAY
|
this.onGoawayFrame(buffer, callback)
|
} else if (header.type === 0x08) { // HEADERS
|
this.onHeaderFrames(buffer, callback)
|
} else if (header.type === 0x09) { // WINDOW_UPDATE
|
this.onWindowUpdateFrame(buffer, callback)
|
} else if (header.type === 0xf000) { // X-FORWARDED
|
this.onXForwardedFrame(buffer, callback)
|
} else {
|
callback(null, { type: 'unknown: ' + header.type })
|
}
|
}
|
|
Parser.prototype._filterHeader = function _filterHeader (headers, name) {
|
var res = {}
|
var keys = Object.keys(headers)
|
|
for (var i = 0; i < keys.length; i++) {
|
var key = keys[i]
|
if (key !== name) {
|
res[key] = headers[key]
|
}
|
}
|
|
return res
|
}
|
|
Parser.prototype.onSynHeadFrame = function onSynHeadFrame (type,
|
flags,
|
body,
|
callback) {
|
var self = this
|
var stream = type === 0x01
|
var offset = stream ? 10 : this.version === 2 ? 6 : 4
|
|
if (!body.has(offset)) {
|
return callback(new Error('SynHead OOB'))
|
}
|
|
var head = body.clone(offset)
|
body.skip(offset)
|
this.parseKVs(body, function (err, headers) {
|
if (err) {
|
return callback(err)
|
}
|
|
if (stream &&
|
(!headers[':method'] || !headers[':path'])) {
|
return callback(new Error('Missing `:method` and/or `:path` header'))
|
}
|
|
var id = head.readUInt32BE() & 0x7fffffff
|
|
if (id === 0) {
|
return callback(self.error(constants.error.PROTOCOL_ERROR,
|
'Invalid stream id for HEADERS'))
|
}
|
|
var associated = stream ? head.readUInt32BE() & 0x7fffffff : 0
|
var priority = stream
|
? head.readUInt8() >> 5
|
: utils.weightToPriority(constants.DEFAULT_WEIGHT)
|
var fin = (flags & constants.flags.FLAG_FIN) !== 0
|
var unidir = (flags & constants.flags.FLAG_UNIDIRECTIONAL) !== 0
|
var path = headers[':path']
|
|
var isPush = stream && associated !== 0
|
|
var weight = utils.priorityToWeight(priority)
|
var priorityInfo = {
|
weight: weight,
|
exclusive: false,
|
parent: 0
|
}
|
|
if (!isPush) {
|
callback(null, {
|
type: 'HEADERS',
|
id: id,
|
priority: priorityInfo,
|
fin: fin,
|
writable: !unidir,
|
headers: headers,
|
path: path
|
})
|
return
|
}
|
|
if (stream && !headers[':status']) {
|
return callback(new Error('Missing `:status` header'))
|
}
|
|
var filteredHeaders = self._filterHeader(headers, ':status')
|
|
callback(null, [ {
|
type: 'PUSH_PROMISE',
|
id: associated,
|
fin: false,
|
promisedId: id,
|
headers: filteredHeaders,
|
path: path
|
}, {
|
type: 'HEADERS',
|
id: id,
|
fin: fin,
|
priority: priorityInfo,
|
writable: true,
|
path: undefined,
|
headers: {
|
':status': headers[':status']
|
}
|
}])
|
})
|
}
|
|
Parser.prototype.onHeaderFrames = function onHeaderFrames (body, callback) {
|
var offset = this.version === 2 ? 6 : 4
|
if (!body.has(offset)) {
|
return callback(new Error('HEADERS OOB'))
|
}
|
|
var streamId = body.readUInt32BE() & 0x7fffffff
|
if (this.version === 2) { body.skip(2) }
|
|
this.parseKVs(body, function (err, headers) {
|
if (err) { return callback(err) }
|
|
callback(null, {
|
type: 'HEADERS',
|
priority: {
|
parent: 0,
|
exclusive: false,
|
weight: constants.DEFAULT_WEIGHT
|
},
|
id: streamId,
|
fin: false,
|
writable: true,
|
path: undefined,
|
headers: headers
|
})
|
})
|
}
|
|
Parser.prototype.parseKVs = function parseKVs (buffer, callback) {
|
var self = this
|
|
this.decompress.write(buffer.toChunks(), function (err, chunks) {
|
if (err) {
|
return callback(err)
|
}
|
|
var buffer = new OffsetBuffer()
|
for (var i = 0; i < chunks.length; i++) {
|
buffer.push(chunks[i])
|
}
|
|
var size = self.version === 2 ? 2 : 4
|
if (!buffer.has(size)) { return callback(new Error('KV OOB')) }
|
|
var count = self.version === 2
|
? buffer.readUInt16BE()
|
: buffer.readUInt32BE()
|
|
var headers = {}
|
|
function readString () {
|
if (!buffer.has(size)) { return null }
|
var len = self.version === 2
|
? buffer.readUInt16BE()
|
: buffer.readUInt32BE()
|
|
if (!buffer.has(len)) { return null }
|
|
var value = buffer.take(len)
|
return value.toString()
|
}
|
|
while (count > 0) {
|
var key = readString()
|
var value = readString()
|
|
if (key === null || value === null) {
|
return callback(new Error('Headers OOB'))
|
}
|
|
if (self.version < 3) {
|
var isInternal = /^(method|version|url|host|scheme|status)$/.test(key)
|
if (key === 'url') {
|
key = 'path'
|
}
|
if (isInternal) {
|
key = ':' + key
|
}
|
}
|
|
// Compatibility with HTTP2
|
if (key === ':status') {
|
value = value.split(/ /g, 2)[0]
|
}
|
|
count--
|
if (key === ':host') {
|
key = ':authority'
|
}
|
|
// Skip version, not present in HTTP2
|
if (key === ':version') {
|
continue
|
}
|
|
value = value.split(/\0/g)
|
for (var j = 0; j < value.length; j++) {
|
utils.addHeaderLine(key, value[j], headers)
|
}
|
}
|
|
callback(null, headers)
|
})
|
}
|
|
Parser.prototype.onRSTFrame = function onRSTFrame (body, callback) {
|
if (!body.has(8)) { return callback(new Error('RST OOB')) }
|
|
var frame = {
|
type: 'RST',
|
id: body.readUInt32BE() & 0x7fffffff,
|
code: constants.errorByCode[body.readUInt32BE()]
|
}
|
|
if (frame.id === 0) {
|
return callback(this.error(constants.error.PROTOCOL_ERROR,
|
'Invalid stream id for RST'))
|
}
|
|
if (body.size !== 0) {
|
frame.extra = body.take(body.size)
|
}
|
callback(null, frame)
|
}
|
|
Parser.prototype.onSettingsFrame = function onSettingsFrame (body, callback) {
|
if (!body.has(4)) {
|
return callback(new Error('SETTINGS OOB'))
|
}
|
|
var settings = {}
|
var number = body.readUInt32BE()
|
var idMap = {
|
1: 'upload_bandwidth',
|
2: 'download_bandwidth',
|
3: 'round_trip_time',
|
4: 'max_concurrent_streams',
|
5: 'current_cwnd',
|
6: 'download_retrans_rate',
|
7: 'initial_window_size',
|
8: 'client_certificate_vector_size'
|
}
|
|
if (!body.has(number * 8)) {
|
return callback(new Error('SETTINGS OOB#2'))
|
}
|
|
for (var i = 0; i < number; i++) {
|
var id = this.version === 2
|
? body.readUInt32LE()
|
: body.readUInt32BE()
|
|
var flags = (id >> 24) & 0xff
|
id = id & 0xffffff
|
|
// Skip persisted settings
|
if (flags & 0x2) { continue }
|
|
var name = idMap[id]
|
|
settings[name] = body.readUInt32BE()
|
}
|
|
callback(null, {
|
type: 'SETTINGS',
|
settings: settings
|
})
|
}
|
|
Parser.prototype.onPingFrame = function onPingFrame (body, callback) {
|
if (!body.has(4)) {
|
return callback(new Error('PING OOB'))
|
}
|
|
var isServer = this.isServer
|
var opaque = body.clone(body.size).take(body.size)
|
var id = body.readUInt32BE()
|
var ack = isServer ? (id % 2 === 0) : (id % 2 === 1)
|
|
callback(null, { type: 'PING', opaque: opaque, ack: ack })
|
}
|
|
Parser.prototype.onGoawayFrame = function onGoawayFrame (body, callback) {
|
if (!body.has(8)) {
|
return callback(new Error('GOAWAY OOB'))
|
}
|
|
callback(null, {
|
type: 'GOAWAY',
|
lastId: body.readUInt32BE() & 0x7fffffff,
|
code: constants.goawayByCode[body.readUInt32BE()]
|
})
|
}
|
|
Parser.prototype.onWindowUpdateFrame = function onWindowUpdateFrame (body,
|
callback) {
|
if (!body.has(8)) {
|
return callback(new Error('WINDOW_UPDATE OOB'))
|
}
|
|
callback(null, {
|
type: 'WINDOW_UPDATE',
|
id: body.readUInt32BE() & 0x7fffffff,
|
delta: body.readInt32BE()
|
})
|
}
|
|
Parser.prototype.onXForwardedFrame = function onXForwardedFrame (body,
|
callback) {
|
if (!body.has(4)) {
|
return callback(new Error('X_FORWARDED OOB'))
|
}
|
|
var len = body.readUInt32BE()
|
if (!body.has(len)) { return callback(new Error('X_FORWARDED host length OOB')) }
|
|
callback(null, {
|
type: 'X_FORWARDED_FOR',
|
host: body.take(len).toString()
|
})
|
}
|