| 'use strict' |
| |
| var transport = require('../../../spdy-transport') |
| var base = transport.protocol.base |
| var constants = require('./').constants |
| |
| var assert = require('assert') |
| var util = require('util') |
| var WriteBuffer = require('wbuf') |
| var OffsetBuffer = require('obuf') |
| var debug = require('debug')('spdy:framer') |
| var debugExtra = require('debug')('spdy:framer:extra') |
| |
| function Framer (options) { |
| base.Framer.call(this, options) |
| |
| this.maxFrameSize = constants.INITIAL_MAX_FRAME_SIZE |
| } |
| util.inherits(Framer, base.Framer) |
| module.exports = Framer |
| |
| Framer.create = function create (options) { |
| return new Framer(options) |
| } |
| |
| Framer.prototype.setMaxFrameSize = function setMaxFrameSize (size) { |
| this.maxFrameSize = size |
| } |
| |
| Framer.prototype._frame = function _frame (frame, body, callback) { |
| debug('id=%d type=%s', frame.id, frame.type) |
| |
| var buffer = new WriteBuffer() |
| |
| buffer.reserve(constants.FRAME_HEADER_SIZE) |
| var len = buffer.skip(3) |
| buffer.writeUInt8(constants.frameType[frame.type]) |
| buffer.writeUInt8(frame.flags) |
| buffer.writeUInt32BE(frame.id & 0x7fffffff) |
| |
| body(buffer) |
| |
| var frameSize = buffer.size - constants.FRAME_HEADER_SIZE |
| len.writeUInt24BE(frameSize) |
| |
| var chunks = buffer.render() |
| var toWrite = { |
| stream: frame.id, |
| priority: frame.priority === undefined ? false : frame.priority, |
| chunks: chunks, |
| callback: callback |
| } |
| |
| if (this.window && frame.type === 'DATA') { |
| var self = this |
| this._resetTimeout() |
| this.window.send.update(-frameSize, function () { |
| self._resetTimeout() |
| self.schedule(toWrite) |
| }) |
| } else { |
| this._resetTimeout() |
| this.schedule(toWrite) |
| } |
| |
| return chunks |
| } |
| |
| Framer.prototype._split = function _split (frame) { |
| var buf = new OffsetBuffer() |
| for (var i = 0; i < frame.chunks.length; i++) { buf.push(frame.chunks[i]) } |
| |
| var frames = [] |
| while (!buf.isEmpty()) { |
| // First frame may have reserved bytes in it |
| var size = this.maxFrameSize |
| if (frames.length === 0) { |
| size -= frame.reserve |
| } |
| size = Math.min(size, buf.size) |
| |
| var frameBuf = buf.clone(size) |
| buf.skip(size) |
| |
| frames.push({ |
| size: frameBuf.size, |
| chunks: frameBuf.toChunks() |
| }) |
| } |
| |
| return frames |
| } |
| |
| Framer.prototype._continuationFrame = function _continuationFrame (frame, |
| body, |
| callback) { |
| var frames = this._split(frame) |
| |
| frames.forEach(function (subFrame, i) { |
| var isFirst = i === 0 |
| var isLast = i === frames.length - 1 |
| |
| var flags = isLast ? constants.flags.END_HEADERS : 0 |
| |
| // PRIORITY and friends |
| if (isFirst) { |
| flags |= frame.flags |
| } |
| |
| this._frame({ |
| id: frame.id, |
| priority: false, |
| type: isFirst ? frame.type : 'CONTINUATION', |
| flags: flags |
| }, function (buf) { |
| // Fill those reserved bytes |
| if (isFirst && body) { body(buf) } |
| |
| buf.reserve(subFrame.size) |
| for (var i = 0; i < subFrame.chunks.length; i++) { buf.copyFrom(subFrame.chunks[i]) } |
| }, isLast ? callback : null) |
| }, this) |
| |
| if (frames.length === 0) { |
| this._frame({ |
| id: frame.id, |
| priority: false, |
| type: frame.type, |
| flags: frame.flags | constants.flags.END_HEADERS |
| }, function (buf) { |
| if (body) { body(buf) } |
| }, callback) |
| } |
| } |
| |
| Framer.prototype._compressHeaders = function _compressHeaders (headers, |
| pairs, |
| callback) { |
| Object.keys(headers || {}).forEach(function (name) { |
| var lowName = name.toLowerCase() |
| |
| // Not allowed in HTTP2 |
| switch (lowName) { |
| case 'host': |
| case 'connection': |
| case 'keep-alive': |
| case 'proxy-connection': |
| case 'transfer-encoding': |
| case 'upgrade': |
| return |
| } |
| |
| // Should be in `pairs` |
| if (/^:/.test(lowName)) { |
| return |
| } |
| |
| // Do not compress, or index Cookie field (for security reasons) |
| var neverIndex = lowName === 'cookie' || lowName === 'set-cookie' |
| |
| var value = headers[name] |
| if (Array.isArray(value)) { |
| for (var i = 0; i < value.length; i++) { |
| pairs.push({ |
| name: lowName, |
| value: value[i] + '', |
| neverIndex: neverIndex, |
| huffman: !neverIndex |
| }) |
| } |
| } else { |
| pairs.push({ |
| name: lowName, |
| value: value + '', |
| neverIndex: neverIndex, |
| huffman: !neverIndex |
| }) |
| } |
| }) |
| |
| assert(this.compress !== null, 'Framer version not initialized') |
| debugExtra('compressing headers=%j', pairs) |
| this.compress.write([ pairs ], callback) |
| } |
| |
| Framer.prototype._isDefaultPriority = function _isDefaultPriority (priority) { |
| if (!priority) { return true } |
| |
| return !priority.parent && |
| priority.weight === constants.DEFAULT && |
| !priority.exclusive |
| } |
| |
| Framer.prototype._defaultHeaders = function _defaultHeaders (frame, pairs) { |
| if (!frame.path) { |
| throw new Error('`path` is required frame argument') |
| } |
| |
| pairs.push({ |
| name: ':method', |
| value: frame.method || base.constants.DEFAULT_METHOD |
| }) |
| pairs.push({ name: ':path', value: frame.path }) |
| pairs.push({ name: ':scheme', value: frame.scheme || 'https' }) |
| pairs.push({ |
| name: ':authority', |
| value: frame.host || |
| (frame.headers && frame.headers.host) || |
| base.constants.DEFAULT_HOST |
| }) |
| } |
| |
| Framer.prototype._headersFrame = function _headersFrame (kind, frame, callback) { |
| var pairs = [] |
| |
| if (kind === 'request') { |
| this._defaultHeaders(frame, pairs) |
| } else if (kind === 'response') { |
| pairs.push({ name: ':status', value: (frame.status || 200) + '' }) |
| } |
| |
| var self = this |
| this._compressHeaders(frame.headers, pairs, function (err, chunks) { |
| if (err) { |
| if (callback) { |
| return callback(err) |
| } else { |
| return self.emit('error', err) |
| } |
| } |
| |
| var reserve = 0 |
| |
| // If priority info is present, and the values are not default ones |
| // reserve space for the priority info and add PRIORITY flag |
| var priority = frame.priority |
| if (!self._isDefaultPriority(priority)) { reserve = 5 } |
| |
| var flags = reserve === 0 ? 0 : constants.flags.PRIORITY |
| |
| // Mostly for testing |
| if (frame.fin) { |
| flags |= constants.flags.END_STREAM |
| } |
| |
| self._continuationFrame({ |
| id: frame.id, |
| type: 'HEADERS', |
| flags: flags, |
| reserve: reserve, |
| chunks: chunks |
| }, function (buf) { |
| if (reserve === 0) { |
| return |
| } |
| |
| buf.writeUInt32BE(((priority.exclusive ? 0x80000000 : 0) | |
| priority.parent) >>> 0) |
| buf.writeUInt8((priority.weight | 0) - 1) |
| }, callback) |
| }) |
| } |
| |
| Framer.prototype.requestFrame = function requestFrame (frame, callback) { |
| return this._headersFrame('request', frame, callback) |
| } |
| |
| Framer.prototype.responseFrame = function responseFrame (frame, callback) { |
| return this._headersFrame('response', frame, callback) |
| } |
| |
| Framer.prototype.headersFrame = function headersFrame (frame, callback) { |
| return this._headersFrame('headers', frame, callback) |
| } |
| |
| Framer.prototype.pushFrame = function pushFrame (frame, callback) { |
| var self = this |
| |
| function compress (headers, pairs, callback) { |
| self._compressHeaders(headers, pairs, function (err, chunks) { |
| if (err) { |
| if (callback) { |
| return callback(err) |
| } else { |
| return self.emit('error', err) |
| } |
| } |
| |
| callback(chunks) |
| }) |
| } |
| |
| function sendPromise (chunks) { |
| self._continuationFrame({ |
| id: frame.id, |
| type: 'PUSH_PROMISE', |
| reserve: 4, |
| chunks: chunks |
| }, function (buf) { |
| buf.writeUInt32BE(frame.promisedId) |
| }) |
| } |
| |
| function sendResponse (chunks, callback) { |
| var priority = frame.priority |
| var isDefaultPriority = self._isDefaultPriority(priority) |
| var flags = isDefaultPriority ? 0 : constants.flags.PRIORITY |
| |
| // Mostly for testing |
| if (frame.fin) { |
| flags |= constants.flags.END_STREAM |
| } |
| |
| self._continuationFrame({ |
| id: frame.promisedId, |
| type: 'HEADERS', |
| flags: flags, |
| reserve: isDefaultPriority ? 0 : 5, |
| chunks: chunks |
| }, function (buf) { |
| if (isDefaultPriority) { |
| return |
| } |
| |
| buf.writeUInt32BE((priority.exclusive ? 0x80000000 : 0) | |
| priority.parent) |
| buf.writeUInt8((priority.weight | 0) - 1) |
| }, callback) |
| } |
| |
| this._checkPush(function (err) { |
| if (err) { |
| return callback(err) |
| } |
| |
| var pairs = { |
| promise: [], |
| response: [] |
| } |
| |
| self._defaultHeaders(frame, pairs.promise) |
| pairs.response.push({ name: ':status', value: (frame.status || 200) + '' }) |
| |
| compress(frame.headers, pairs.promise, function (promiseChunks) { |
| sendPromise(promiseChunks) |
| if (frame.response === false) { |
| return callback(null) |
| } |
| compress(frame.response, pairs.response, function (responseChunks) { |
| sendResponse(responseChunks, callback) |
| }) |
| }) |
| }) |
| } |
| |
| Framer.prototype.priorityFrame = function priorityFrame (frame, callback) { |
| this._frame({ |
| id: frame.id, |
| priority: false, |
| type: 'PRIORITY', |
| flags: 0 |
| }, function (buf) { |
| var priority = frame.priority |
| buf.writeUInt32BE((priority.exclusive ? 0x80000000 : 0) | |
| priority.parent) |
| buf.writeUInt8((priority.weight | 0) - 1) |
| }, callback) |
| } |
| |
| Framer.prototype.dataFrame = function dataFrame (frame, callback) { |
| var frames = this._split({ |
| reserve: 0, |
| chunks: [ frame.data ] |
| }) |
| |
| var fin = frame.fin ? constants.flags.END_STREAM : 0 |
| |
| var self = this |
| frames.forEach(function (subFrame, i) { |
| var isLast = i === frames.length - 1 |
| var flags = 0 |
| if (isLast) { |
| flags |= fin |
| } |
| |
| self._frame({ |
| id: frame.id, |
| priority: frame.priority, |
| type: 'DATA', |
| flags: flags |
| }, function (buf) { |
| buf.reserve(subFrame.size) |
| for (var i = 0; i < subFrame.chunks.length; i++) { buf.copyFrom(subFrame.chunks[i]) } |
| }, isLast ? callback : null) |
| }) |
| |
| // Empty DATA |
| if (frames.length === 0) { |
| this._frame({ |
| id: frame.id, |
| priority: frame.priority, |
| type: 'DATA', |
| flags: fin |
| }, function (buf) { |
| // No-op |
| }, callback) |
| } |
| } |
| |
| Framer.prototype.pingFrame = function pingFrame (frame, callback) { |
| this._frame({ |
| id: 0, |
| type: 'PING', |
| flags: frame.ack ? constants.flags.ACK : 0 |
| }, function (buf) { |
| buf.copyFrom(frame.opaque) |
| }, callback) |
| } |
| |
| Framer.prototype.rstFrame = function rstFrame (frame, callback) { |
| this._frame({ |
| id: frame.id, |
| type: 'RST_STREAM', |
| flags: 0 |
| }, function (buf) { |
| buf.writeUInt32BE(constants.error[frame.code]) |
| }, callback) |
| } |
| |
| Framer.prototype.prefaceFrame = function prefaceFrame (callback) { |
| debug('preface') |
| this._resetTimeout() |
| this.schedule({ |
| stream: 0, |
| priority: false, |
| chunks: [ constants.PREFACE_BUFFER ], |
| callback: callback |
| }) |
| } |
| |
| Framer.prototype.settingsFrame = function settingsFrame (options, callback) { |
| var key = JSON.stringify(options) |
| |
| var settings = Framer.settingsCache[key] |
| if (settings) { |
| debug('cached settings') |
| this._resetTimeout() |
| this.schedule({ |
| id: 0, |
| priority: false, |
| chunks: settings, |
| callback: callback |
| }) |
| return |
| } |
| |
| var params = [] |
| for (var i = 0; i < constants.settingsIndex.length; i++) { |
| var name = constants.settingsIndex[i] |
| if (!name) { |
| continue |
| } |
| |
| // value: Infinity |
| if (!isFinite(options[name])) { |
| continue |
| } |
| |
| if (options[name] !== undefined) { |
| params.push({ key: i, value: options[name] }) |
| } |
| } |
| |
| var bodySize = params.length * 6 |
| |
| var chunks = this._frame({ |
| id: 0, |
| type: 'SETTINGS', |
| flags: 0 |
| }, function (buffer) { |
| buffer.reserve(bodySize) |
| for (var i = 0; i < params.length; i++) { |
| var param = params[i] |
| |
| buffer.writeUInt16BE(param.key) |
| buffer.writeUInt32BE(param.value) |
| } |
| }, callback) |
| |
| Framer.settingsCache[key] = chunks |
| } |
| Framer.settingsCache = {} |
| |
| Framer.prototype.ackSettingsFrame = function ackSettingsFrame (callback) { |
| /* var chunks = */ this._frame({ |
| id: 0, |
| type: 'SETTINGS', |
| flags: constants.flags.ACK |
| }, function (buffer) { |
| // No-op |
| }, callback) |
| } |
| |
| Framer.prototype.windowUpdateFrame = function windowUpdateFrame (frame, |
| callback) { |
| this._frame({ |
| id: frame.id, |
| type: 'WINDOW_UPDATE', |
| flags: 0 |
| }, function (buffer) { |
| buffer.reserve(4) |
| buffer.writeInt32BE(frame.delta) |
| }, callback) |
| } |
| |
| Framer.prototype.goawayFrame = function goawayFrame (frame, callback) { |
| this._frame({ |
| type: 'GOAWAY', |
| id: 0, |
| flags: 0 |
| }, function (buf) { |
| buf.reserve(8) |
| |
| // Last-good-stream-ID |
| buf.writeUInt32BE(frame.lastId & 0x7fffffff) |
| // Code |
| buf.writeUInt32BE(constants.goaway[frame.code]) |
| |
| // Extra debugging information |
| if (frame.extra) { buf.write(frame.extra) } |
| }, callback) |
| } |
| |
| Framer.prototype.xForwardedFor = function xForwardedFor (frame, callback) { |
| this._frame({ |
| type: 'X_FORWARDED_FOR', |
| id: 0, |
| flags: 0 |
| }, function (buf) { |
| buf.write(frame.host) |
| }, callback) |
| } |