'use strict'
|
|
var transport = require('../../../spdy-transport')
|
var constants = require('./').constants
|
var base = transport.protocol.base
|
var utils = base.utils
|
|
var assert = require('assert')
|
var util = require('util')
|
var Buffer = require('buffer').Buffer
|
var WriteBuffer = require('wbuf')
|
|
var debug = require('debug')('spdy:framer')
|
|
function Framer (options) {
|
base.Framer.call(this, options)
|
}
|
util.inherits(Framer, base.Framer)
|
module.exports = Framer
|
|
Framer.create = function create (options) {
|
return new Framer(options)
|
}
|
|
Framer.prototype.setMaxFrameSize = function setMaxFrameSize (size) {
|
// http2-only
|
}
|
|
Framer.prototype.headersToDict = function headersToDict (headers,
|
preprocess,
|
callback) {
|
function stringify (value) {
|
if (value !== undefined) {
|
if (Array.isArray(value)) {
|
return value.join('\x00')
|
} else if (typeof value === 'string') {
|
return value
|
} else {
|
return value.toString()
|
}
|
} else {
|
return ''
|
}
|
}
|
|
// Lower case of all headers keys
|
var loweredHeaders = {}
|
Object.keys(headers || {}).map(function (key) {
|
loweredHeaders[key.toLowerCase()] = headers[key]
|
})
|
|
// Allow outer code to add custom headers or remove something
|
if (preprocess) { preprocess(loweredHeaders) }
|
|
// Transform object into kv pairs
|
var size = this.version === 2 ? 2 : 4
|
var len = size
|
var pairs = Object.keys(loweredHeaders).filter(function (key) {
|
var lkey = key.toLowerCase()
|
|
// Will be in `:host`
|
if (lkey === 'host' && this.version >= 3) {
|
return false
|
}
|
|
return lkey !== 'connection' && lkey !== 'keep-alive' &&
|
lkey !== 'proxy-connection' && lkey !== 'transfer-encoding'
|
}, this).map(function (key) {
|
var klen = Buffer.byteLength(key)
|
var value = stringify(loweredHeaders[key])
|
var vlen = Buffer.byteLength(value)
|
|
len += size * 2 + klen + vlen
|
return [klen, key, vlen, value]
|
})
|
|
var block = new WriteBuffer()
|
block.reserve(len)
|
|
if (this.version === 2) {
|
block.writeUInt16BE(pairs.length)
|
} else {
|
block.writeUInt32BE(pairs.length)
|
}
|
|
pairs.forEach(function (pair) {
|
// Write key length
|
if (this.version === 2) {
|
block.writeUInt16BE(pair[0])
|
} else {
|
block.writeUInt32BE(pair[0])
|
}
|
|
// Write key
|
block.write(pair[1])
|
|
// Write value length
|
if (this.version === 2) {
|
block.writeUInt16BE(pair[2])
|
} else {
|
block.writeUInt32BE(pair[2])
|
}
|
// Write value
|
block.write(pair[3])
|
}, this)
|
|
assert(this.compress !== null, 'Framer version not initialized')
|
this.compress.write(block.render(), callback)
|
}
|
|
Framer.prototype._frame = function _frame (frame, body, callback) {
|
if (!this.version) {
|
this.on('version', function () {
|
this._frame(frame, body, callback)
|
})
|
return
|
}
|
|
debug('id=%d type=%s', frame.id, frame.type)
|
|
var buffer = new WriteBuffer()
|
|
buffer.writeUInt16BE(0x8000 | this.version)
|
buffer.writeUInt16BE(constants.frameType[frame.type])
|
buffer.writeUInt8(frame.flags)
|
var len = buffer.skip(3)
|
|
body(buffer)
|
|
var frameSize = buffer.size - constants.FRAME_HEADER_SIZE
|
len.writeUInt24BE(frameSize)
|
|
var chunks = buffer.render()
|
var toWrite = {
|
stream: frame.id,
|
priority: false,
|
chunks: chunks,
|
callback: callback
|
}
|
|
this._resetTimeout()
|
this.schedule(toWrite)
|
|
return chunks
|
}
|
|
Framer.prototype._synFrame = function _synFrame (frame, callback) {
|
var self = this
|
|
if (!frame.path) {
|
throw new Error('`path` is required frame argument')
|
}
|
|
function preprocess (headers) {
|
var method = frame.method || base.constants.DEFAULT_METHOD
|
var version = frame.version || 'HTTP/1.1'
|
var scheme = frame.scheme || 'https'
|
var host = frame.host ||
|
(frame.headers && frame.headers.host) ||
|
base.constants.DEFAULT_HOST
|
|
if (self.version === 2) {
|
headers.method = method
|
headers.version = version
|
headers.url = frame.path
|
headers.scheme = scheme
|
headers.host = host
|
if (frame.status) {
|
headers.status = frame.status
|
}
|
} else {
|
headers[':method'] = method
|
headers[':version'] = version
|
headers[':path'] = frame.path
|
headers[':scheme'] = scheme
|
headers[':host'] = host
|
if (frame.status) { headers[':status'] = frame.status }
|
}
|
}
|
|
this.headersToDict(frame.headers, preprocess, function (err, chunks) {
|
if (err) {
|
if (callback) {
|
return callback(err)
|
} else {
|
return self.emit('error', err)
|
}
|
}
|
|
self._frame({
|
type: 'SYN_STREAM',
|
id: frame.id,
|
flags: frame.fin ? constants.flags.FLAG_FIN : 0
|
}, function (buf) {
|
buf.reserve(10)
|
|
buf.writeUInt32BE(frame.id & 0x7fffffff)
|
buf.writeUInt32BE(frame.associated & 0x7fffffff)
|
|
var weight = (frame.priority && frame.priority.weight) ||
|
constants.DEFAULT_WEIGHT
|
|
// We only have 3 bits for priority in SPDY, try to fit it into this
|
var priority = utils.weightToPriority(weight)
|
buf.writeUInt8(priority << 5)
|
|
// CREDENTIALS slot
|
buf.writeUInt8(0)
|
|
for (var i = 0; i < chunks.length; i++) {
|
buf.copyFrom(chunks[i])
|
}
|
}, callback)
|
})
|
}
|
|
Framer.prototype.requestFrame = function requestFrame (frame, callback) {
|
this._synFrame({
|
id: frame.id,
|
fin: frame.fin,
|
associated: 0,
|
method: frame.method,
|
version: frame.version,
|
scheme: frame.scheme,
|
host: frame.host,
|
path: frame.path,
|
priority: frame.priority,
|
headers: frame.headers
|
}, callback)
|
}
|
|
Framer.prototype.responseFrame = function responseFrame (frame, callback) {
|
var self = this
|
|
var reason = frame.reason
|
if (!reason) {
|
reason = constants.statusReason[frame.status]
|
}
|
|
function preprocess (headers) {
|
if (self.version === 2) {
|
headers.status = frame.status + ' ' + reason
|
headers.version = 'HTTP/1.1'
|
} else {
|
headers[':status'] = frame.status + ' ' + reason
|
headers[':version'] = 'HTTP/1.1'
|
}
|
}
|
|
this.headersToDict(frame.headers, preprocess, function (err, chunks) {
|
if (err) {
|
if (callback) {
|
return callback(err)
|
} else {
|
return self.emit('error', err)
|
}
|
}
|
|
self._frame({
|
type: 'SYN_REPLY',
|
id: frame.id,
|
flags: 0
|
}, function (buf) {
|
buf.reserve(self.version === 2 ? 6 : 4)
|
|
buf.writeUInt32BE(frame.id & 0x7fffffff)
|
|
// Unused data
|
if (self.version === 2) {
|
buf.writeUInt16BE(0)
|
}
|
|
for (var i = 0; i < chunks.length; i++) {
|
buf.copyFrom(chunks[i])
|
}
|
}, callback)
|
})
|
}
|
|
Framer.prototype.pushFrame = function pushFrame (frame, callback) {
|
var self = this
|
|
this._checkPush(function (err) {
|
if (err) { return callback(err) }
|
|
self._synFrame({
|
id: frame.promisedId,
|
associated: frame.id,
|
method: frame.method,
|
status: frame.status || 200,
|
version: frame.version,
|
scheme: frame.scheme,
|
host: frame.host,
|
path: frame.path,
|
priority: frame.priority,
|
|
// Merge everything together, there is no difference in SPDY protocol
|
headers: Object.assign(Object.assign({}, frame.headers), frame.response)
|
}, callback)
|
})
|
}
|
|
Framer.prototype.headersFrame = function headersFrame (frame, callback) {
|
var self = this
|
|
this.headersToDict(frame.headers, null, function (err, chunks) {
|
if (err) {
|
if (callback) { return callback(err) } else {
|
return self.emit('error', err)
|
}
|
}
|
|
self._frame({
|
type: 'HEADERS',
|
id: frame.id,
|
priority: false,
|
flags: 0
|
}, function (buf) {
|
buf.reserve(4 + (self.version === 2 ? 2 : 0))
|
buf.writeUInt32BE(frame.id & 0x7fffffff)
|
|
// Unused data
|
if (self.version === 2) { buf.writeUInt16BE(0) }
|
|
for (var i = 0; i < chunks.length; i++) {
|
buf.copyFrom(chunks[i])
|
}
|
}, callback)
|
})
|
}
|
|
Framer.prototype.dataFrame = function dataFrame (frame, callback) {
|
if (!this.version) {
|
return this.on('version', function () {
|
this.dataFrame(frame, callback)
|
})
|
}
|
|
debug('id=%d type=DATA', frame.id)
|
|
var buffer = new WriteBuffer()
|
buffer.reserve(8 + frame.data.length)
|
|
buffer.writeUInt32BE(frame.id & 0x7fffffff)
|
buffer.writeUInt8(frame.fin ? 0x01 : 0x0)
|
buffer.writeUInt24BE(frame.data.length)
|
buffer.copyFrom(frame.data)
|
|
var chunks = buffer.render()
|
var toWrite = {
|
stream: frame.id,
|
priority: frame.priority,
|
chunks: chunks,
|
callback: callback
|
}
|
|
var self = this
|
this._resetTimeout()
|
|
var bypass = this.version < 3.1
|
this.window.send.update(-frame.data.length, bypass ? undefined : function () {
|
self._resetTimeout()
|
self.schedule(toWrite)
|
})
|
|
if (bypass) {
|
this._resetTimeout()
|
this.schedule(toWrite)
|
}
|
}
|
|
Framer.prototype.pingFrame = function pingFrame (frame, callback) {
|
this._frame({
|
type: 'PING',
|
id: 0,
|
flags: 0
|
}, function (buf, callback) {
|
buf.reserve(4)
|
|
var opaque = frame.opaque
|
buf.writeUInt32BE(opaque.readUInt32BE(opaque.length - 4, true))
|
}, callback)
|
}
|
|
Framer.prototype.rstFrame = function rstFrame (frame, callback) {
|
this._frame({
|
type: 'RST_STREAM',
|
id: frame.id,
|
flags: 0
|
}, function (buf) {
|
buf.reserve(8)
|
|
// Stream ID
|
buf.writeUInt32BE(frame.id & 0x7fffffff)
|
// Status Code
|
buf.writeUInt32BE(constants.error[frame.code])
|
|
// Extra debugging information
|
if (frame.extra) {
|
buf.write(frame.extra)
|
}
|
}, callback)
|
}
|
|
Framer.prototype.prefaceFrame = function prefaceFrame () {
|
}
|
|
Framer.prototype.settingsFrame = function settingsFrame (options, callback) {
|
var self = this
|
|
var key = this.version + '/' + JSON.stringify(options)
|
|
var settings = Framer.settingsCache[key]
|
if (settings) {
|
debug('cached settings')
|
this._resetTimeout()
|
this.schedule({
|
stream: 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 frame = this._frame({
|
type: 'SETTINGS',
|
id: 0,
|
flags: 0
|
}, function (buf) {
|
buf.reserve(4 + 8 * params.length)
|
|
// Count of entries
|
buf.writeUInt32BE(params.length)
|
|
params.forEach(function (param) {
|
var flag = constants.settings.FLAG_SETTINGS_PERSIST_VALUE << 24
|
|
if (self.version === 2) {
|
buf.writeUInt32LE(flag | param.key)
|
} else { buf.writeUInt32BE(flag | param.key) }
|
buf.writeUInt32BE(param.value & 0x7fffffff)
|
})
|
}, callback)
|
|
Framer.settingsCache[key] = frame
|
}
|
Framer.settingsCache = {}
|
|
Framer.prototype.ackSettingsFrame = function ackSettingsFrame (callback) {
|
if (callback) {
|
process.nextTick(callback)
|
}
|
}
|
|
Framer.prototype.windowUpdateFrame = function windowUpdateFrame (frame,
|
callback) {
|
this._frame({
|
type: 'WINDOW_UPDATE',
|
id: frame.id,
|
flags: 0
|
}, function (buf) {
|
buf.reserve(8)
|
|
// ID
|
buf.writeUInt32BE(frame.id & 0x7fffffff)
|
|
// Delta
|
buf.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)
|
// Status
|
buf.writeUInt32BE(constants.goaway[frame.code])
|
}, callback)
|
}
|
|
Framer.prototype.priorityFrame = function priorityFrame (frame, callback) {
|
// No such thing in SPDY
|
if (callback) {
|
process.nextTick(callback)
|
}
|
}
|
|
Framer.prototype.xForwardedFor = function xForwardedFor (frame, callback) {
|
this._frame({
|
type: 'X_FORWARDED_FOR',
|
id: 0,
|
flags: 0
|
}, function (buf) {
|
buf.writeUInt32BE(Buffer.byteLength(frame.host))
|
buf.write(frame.host)
|
}, callback)
|
}
|