| '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() |
| }) |
| } |