Updated the files.

This commit is contained in:
Batuhan Berk Başoğlu 2024-02-08 19:38:41 -05:00
parent 1553e6b971
commit 753967d4f5
23418 changed files with 3784666 additions and 0 deletions

25
my-app/node_modules/spdy-transport/lib/spdy-transport.js generated vendored Executable file
View file

@ -0,0 +1,25 @@
'use strict'
var transport = exports
// Exports utils
transport.utils = require('./spdy-transport/utils')
// Export parser&framer
transport.protocol = {}
transport.protocol.base = require('./spdy-transport/protocol/base')
transport.protocol.spdy = require('./spdy-transport/protocol/spdy')
transport.protocol.http2 = require('./spdy-transport/protocol/http2')
// Window
transport.Window = require('./spdy-transport/window')
// Priority Tree
transport.Priority = require('./spdy-transport/priority')
// Export Connection and Stream
transport.Stream = require('./spdy-transport/stream').Stream
transport.Connection = require('./spdy-transport/connection').Connection
// Just for `transport.connection.create()`
transport.connection = transport.Connection

View file

@ -0,0 +1,845 @@
'use strict'
var util = require('util')
var transport = require('../spdy-transport')
var debug = {
server: require('debug')('spdy:connection:server'),
client: require('debug')('spdy:connection:client')
}
var EventEmitter = require('events').EventEmitter
var Stream = transport.Stream
function Connection (socket, options) {
EventEmitter.call(this)
var state = {}
this._spdyState = state
// NOTE: There's a big trick here. Connection is used as a `this` argument
// to the wrapped `connection` event listener.
// socket end doesn't necessarly mean connection drop
this.httpAllowHalfOpen = true
state.timeout = new transport.utils.Timeout(this)
// Protocol info
state.protocol = transport.protocol[options.protocol]
state.version = null
state.constants = state.protocol.constants
state.pair = null
state.isServer = options.isServer
// Root of priority tree (i.e. stream id = 0)
state.priorityRoot = new transport.Priority({
defaultWeight: state.constants.DEFAULT_WEIGHT,
maxCount: transport.protocol.base.constants.MAX_PRIORITY_STREAMS
})
// Defaults
state.maxStreams = options.maxStreams ||
state.constants.MAX_CONCURRENT_STREAMS
state.autoSpdy31 = options.protocol.name !== 'h2' && options.autoSpdy31
state.acceptPush = options.acceptPush === undefined
? !state.isServer
: options.acceptPush
if (options.maxChunk === false) { state.maxChunk = Infinity } else if (options.maxChunk === undefined) { state.maxChunk = transport.protocol.base.constants.DEFAULT_MAX_CHUNK } else {
state.maxChunk = options.maxChunk
}
// Connection-level flow control
var windowSize = options.windowSize || 1 << 20
state.window = new transport.Window({
id: 0,
isServer: state.isServer,
recv: {
size: state.constants.DEFAULT_WINDOW,
max: state.constants.MAX_INITIAL_WINDOW_SIZE
},
send: {
size: state.constants.DEFAULT_WINDOW,
max: state.constants.MAX_INITIAL_WINDOW_SIZE
}
})
// It starts with DEFAULT_WINDOW, update must be sent to change it on client
state.window.recv.setMax(windowSize)
// Boilerplate for Stream constructor
state.streamWindow = new transport.Window({
id: -1,
isServer: state.isServer,
recv: {
size: windowSize,
max: state.constants.MAX_INITIAL_WINDOW_SIZE
},
send: {
size: state.constants.DEFAULT_WINDOW,
max: state.constants.MAX_INITIAL_WINDOW_SIZE
}
})
// Various state info
state.pool = state.protocol.compressionPool.create(options.headerCompression)
state.counters = {
push: 0,
stream: 0
}
// Init streams list
state.stream = {
map: {},
count: 0,
nextId: state.isServer ? 2 : 1,
lastId: {
both: 0,
received: 0
}
}
state.ping = {
nextId: state.isServer ? 2 : 1,
map: {}
}
state.goaway = false
// Debug
state.debug = state.isServer ? debug.server : debug.client
// X-Forwarded feature
state.xForward = null
// Create parser and hole for framer
state.parser = state.protocol.parser.create({
// NOTE: needed to distinguish ping from ping ACK in SPDY
isServer: state.isServer,
window: state.window
})
state.framer = state.protocol.framer.create({
window: state.window,
timeout: state.timeout
})
// SPDY has PUSH enabled on servers
if (state.protocol.name === 'spdy') {
state.framer.enablePush(state.isServer)
}
if (!state.isServer) { state.parser.skipPreface() }
this.socket = socket
this._init()
}
util.inherits(Connection, EventEmitter)
exports.Connection = Connection
Connection.create = function create (socket, options) {
return new Connection(socket, options)
}
Connection.prototype._init = function init () {
var self = this
var state = this._spdyState
var pool = state.pool
// Initialize session window
state.window.recv.on('drain', function () {
self._onSessionWindowDrain()
})
// Initialize parser
state.parser.on('data', function (frame) {
self._handleFrame(frame)
})
state.parser.once('version', function (version) {
self._onVersion(version)
})
// Propagate parser errors
state.parser.on('error', function (err) {
self._onParserError(err)
})
// Propagate framer errors
state.framer.on('error', function (err) {
self.emit('error', err)
})
this.socket.pipe(state.parser)
state.framer.pipe(this.socket)
// Allow high-level api to catch socket errors
this.socket.on('error', function onSocketError (e) {
self.emit('error', e)
})
this.socket.once('close', function onclose (hadError) {
var err
if (hadError) {
err = new Error('socket hang up')
err.code = 'ECONNRESET'
}
self.destroyStreams(err)
self.emit('close')
if (state.pair) {
pool.put(state.pair)
}
state.framer.resume()
})
// Reset timeout on close
this.once('close', function () {
self.setTimeout(0)
})
function _onWindowOverflow () {
self._onWindowOverflow()
}
state.window.recv.on('overflow', _onWindowOverflow)
state.window.send.on('overflow', _onWindowOverflow)
// Do not allow half-open connections
this.socket.allowHalfOpen = false
}
Connection.prototype._onVersion = function _onVersion (version) {
var state = this._spdyState
var prev = state.version
var parser = state.parser
var framer = state.framer
var pool = state.pool
state.version = version
state.debug('id=0 version=%d', version)
// Ignore transition to 3.1
if (!prev) {
state.pair = pool.get(version)
parser.setCompression(state.pair)
framer.setCompression(state.pair)
}
framer.setVersion(version)
if (!state.isServer) {
framer.prefaceFrame()
if (state.xForward !== null) {
framer.xForwardedFor({ host: state.xForward })
}
}
// Send preface+settings frame (once)
framer.settingsFrame({
max_header_list_size: state.constants.DEFAULT_MAX_HEADER_LIST_SIZE,
max_concurrent_streams: state.maxStreams,
enable_push: state.acceptPush ? 1 : 0,
initial_window_size: state.window.recv.max
})
// Update session window
if (state.version >= 3.1 || (state.isServer && state.autoSpdy31)) { this._onSessionWindowDrain() }
this.emit('version', version)
}
Connection.prototype._onParserError = function _onParserError (err) {
var state = this._spdyState
// Prevent further errors
state.parser.kill()
// Send GOAWAY
if (err instanceof transport.protocol.base.utils.ProtocolError) {
this._goaway({
lastId: state.stream.lastId.both,
code: err.code,
extra: err.message,
send: true
})
}
this.emit('error', err)
}
Connection.prototype._handleFrame = function _handleFrame (frame) {
var state = this._spdyState
state.debug('id=0 frame', frame)
state.timeout.reset()
// For testing purposes
this.emit('frame', frame)
var stream
// Session window update
if (frame.type === 'WINDOW_UPDATE' && frame.id === 0) {
if (state.version < 3.1 && state.autoSpdy31) {
state.debug('id=0 switch version to 3.1')
state.version = 3.1
this.emit('version', 3.1)
}
state.window.send.update(frame.delta)
return
}
if (state.isServer && frame.type === 'PUSH_PROMISE') {
state.debug('id=0 server PUSH_PROMISE')
this._goaway({
lastId: state.stream.lastId.both,
code: 'PROTOCOL_ERROR',
send: true
})
return
}
if (!stream && frame.id !== undefined) {
// Load created one
stream = state.stream.map[frame.id]
// Fail if not found
if (!stream &&
frame.type !== 'HEADERS' &&
frame.type !== 'PRIORITY' &&
frame.type !== 'RST') {
// Other side should destroy the stream upon receiving GOAWAY
if (this._isGoaway(frame.id)) { return }
state.debug('id=0 stream=%d not found', frame.id)
state.framer.rstFrame({ id: frame.id, code: 'INVALID_STREAM' })
return
}
}
// Create new stream
if (!stream && frame.type === 'HEADERS') {
this._handleHeaders(frame)
return
}
if (stream) {
stream._handleFrame(frame)
} else if (frame.type === 'SETTINGS') {
this._handleSettings(frame.settings)
} else if (frame.type === 'ACK_SETTINGS') {
// TODO(indutny): handle it one day
} else if (frame.type === 'PING') {
this._handlePing(frame)
} else if (frame.type === 'GOAWAY') {
this._handleGoaway(frame)
} else if (frame.type === 'X_FORWARDED_FOR') {
// Set X-Forwarded-For only once
if (state.xForward === null) {
state.xForward = frame.host
}
} else if (frame.type === 'PRIORITY') {
// TODO(indutny): handle this
} else {
state.debug('id=0 unknown frame type: %s', frame.type)
}
}
Connection.prototype._onWindowOverflow = function _onWindowOverflow () {
var state = this._spdyState
state.debug('id=0 window overflow')
this._goaway({
lastId: state.stream.lastId.both,
code: 'FLOW_CONTROL_ERROR',
send: true
})
}
Connection.prototype._isGoaway = function _isGoaway (id) {
var state = this._spdyState
if (state.goaway !== false && state.goaway < id) { return true }
return false
}
Connection.prototype._getId = function _getId () {
var state = this._spdyState
var id = state.stream.nextId
state.stream.nextId += 2
return id
}
Connection.prototype._createStream = function _createStream (uri) {
var state = this._spdyState
var id = uri.id
if (id === undefined) { id = this._getId() }
var isGoaway = this._isGoaway(id)
if (uri.push && !state.acceptPush) {
state.debug('id=0 push disabled promisedId=%d', id)
// Fatal error
this._goaway({
lastId: state.stream.lastId.both,
code: 'PROTOCOL_ERROR',
send: true
})
isGoaway = true
}
var stream = new Stream(this, {
id: id,
request: uri.request !== false,
method: uri.method,
path: uri.path,
host: uri.host,
priority: uri.priority,
headers: uri.headers,
parent: uri.parent,
readable: !isGoaway && uri.readable,
writable: !isGoaway && uri.writable
})
var self = this
// Just an empty stream for API consistency
if (isGoaway) {
return stream
}
state.stream.lastId.both = Math.max(state.stream.lastId.both, id)
state.debug('id=0 add stream=%d', stream.id)
state.stream.map[stream.id] = stream
state.stream.count++
state.counters.stream++
if (stream.parent !== null) {
state.counters.push++
}
stream.once('close', function () {
self._removeStream(stream)
})
return stream
}
Connection.prototype._handleHeaders = function _handleHeaders (frame) {
var state = this._spdyState
// Must be HEADERS frame after stream close
if (frame.id <= state.stream.lastId.received) { return }
// Someone is using our ids!
if ((frame.id + state.stream.nextId) % 2 === 0) {
state.framer.rstFrame({ id: frame.id, code: 'PROTOCOL_ERROR' })
return
}
var stream = this._createStream({
id: frame.id,
request: false,
method: frame.headers[':method'],
path: frame.headers[':path'],
host: frame.headers[':authority'],
priority: frame.priority,
headers: frame.headers,
writable: frame.writable
})
// GOAWAY
if (this._isGoaway(stream.id)) {
return
}
state.stream.lastId.received = Math.max(
state.stream.lastId.received,
stream.id
)
// TODO(indutny) handle stream limit
if (!this.emit('stream', stream)) {
// No listeners was set - abort the stream
stream.abort()
return
}
// Create fake frame to simulate end of the data
if (frame.fin) {
stream._handleFrame({ type: 'FIN', fin: true })
}
return stream
}
Connection.prototype._onSessionWindowDrain = function _onSessionWindowDrain () {
var state = this._spdyState
if (state.version < 3.1 && !(state.isServer && state.autoSpdy31)) {
return
}
var delta = state.window.recv.getDelta()
if (delta === 0) {
return
}
state.debug('id=0 session window drain, update by %d', delta)
state.framer.windowUpdateFrame({
id: 0,
delta: delta
})
state.window.recv.update(delta)
}
Connection.prototype.start = function start (version) {
this._spdyState.parser.setVersion(version)
}
// Mostly for testing
Connection.prototype.getVersion = function getVersion () {
return this._spdyState.version
}
Connection.prototype._handleSettings = function _handleSettings (settings) {
var state = this._spdyState
state.framer.ackSettingsFrame()
this._setDefaultWindow(settings)
if (settings.max_frame_size) { state.framer.setMaxFrameSize(settings.max_frame_size) }
// TODO(indutny): handle max_header_list_size
if (settings.header_table_size) {
try {
state.pair.compress.updateTableSize(settings.header_table_size)
} catch (e) {
this._goaway({
lastId: 0,
code: 'PROTOCOL_ERROR',
send: true
})
return
}
}
// HTTP2 clients needs to enable PUSH streams explicitly
if (state.protocol.name !== 'spdy') {
if (settings.enable_push === undefined) {
state.framer.enablePush(state.isServer)
} else {
state.framer.enablePush(settings.enable_push === 1)
}
}
// TODO(indutny): handle max_concurrent_streams
}
Connection.prototype._setDefaultWindow = function _setDefaultWindow (settings) {
if (settings.initial_window_size === undefined) {
return
}
var state = this._spdyState
// Update defaults
var window = state.streamWindow
window.send.setMax(settings.initial_window_size)
// Update existing streams
Object.keys(state.stream.map).forEach(function (id) {
var stream = state.stream.map[id]
var window = stream._spdyState.window
window.send.updateMax(settings.initial_window_size)
})
}
Connection.prototype._handlePing = function handlePing (frame) {
var self = this
var state = this._spdyState
// Handle incoming PING
if (!frame.ack) {
state.framer.pingFrame({
opaque: frame.opaque,
ack: true
})
self.emit('ping', frame.opaque)
return
}
// Handle reply PING
var hex = frame.opaque.toString('hex')
if (!state.ping.map[hex]) {
return
}
var ping = state.ping.map[hex]
delete state.ping.map[hex]
if (ping.cb) {
ping.cb(null)
}
}
Connection.prototype._handleGoaway = function handleGoaway (frame) {
this._goaway({
lastId: frame.lastId,
code: frame.code,
send: false
})
}
Connection.prototype.ping = function ping (callback) {
var state = this._spdyState
// HTTP2 is using 8-byte opaque
var opaque = Buffer.alloc(state.constants.PING_OPAQUE_SIZE)
opaque.fill(0)
opaque.writeUInt32BE(state.ping.nextId, opaque.length - 4)
state.ping.nextId += 2
state.ping.map[opaque.toString('hex')] = { cb: callback }
state.framer.pingFrame({
opaque: opaque,
ack: false
})
}
Connection.prototype.getCounter = function getCounter (name) {
return this._spdyState.counters[name]
}
Connection.prototype.reserveStream = function reserveStream (uri, callback) {
var stream = this._createStream(uri)
// GOAWAY
if (this._isGoaway(stream.id)) {
var err = new Error('Can\'t send request after GOAWAY')
process.nextTick(function () {
if (callback) { callback(err) } else {
stream.emit('error', err)
}
})
return stream
}
if (callback) {
process.nextTick(function () {
callback(null, stream)
})
}
return stream
}
Connection.prototype.request = function request (uri, callback) {
var stream = this.reserveStream(uri, function (err) {
if (err) {
if (callback) {
callback(err)
} else {
stream.emit('error', err)
}
return
}
if (stream._wasSent()) {
if (callback) {
callback(null, stream)
}
return
}
stream.send(function (err) {
if (err) {
if (callback) { return callback(err) } else { return stream.emit('error', err) }
}
if (callback) {
callback(null, stream)
}
})
})
return stream
}
Connection.prototype._removeStream = function _removeStream (stream) {
var state = this._spdyState
state.debug('id=0 remove stream=%d', stream.id)
delete state.stream.map[stream.id]
state.stream.count--
if (state.stream.count === 0) {
this.emit('_streamDrain')
}
}
Connection.prototype._goaway = function _goaway (params) {
var state = this._spdyState
var self = this
state.goaway = params.lastId
state.debug('id=0 goaway from=%d', state.goaway)
Object.keys(state.stream.map).forEach(function (id) {
var stream = state.stream.map[id]
// Abort every stream started after GOAWAY
if (stream.id <= params.lastId) {
return
}
stream.abort()
stream.emit('error', new Error('New stream after GOAWAY'))
})
function finish () {
// Destroy socket if there are no streams
if (state.stream.count === 0 || params.code !== 'OK') {
// No further frames should be processed
state.parser.kill()
process.nextTick(function () {
var err = new Error('Fatal error: ' + params.code)
self._onStreamDrain(err)
})
return
}
self.on('_streamDrain', self._onStreamDrain)
}
if (params.send) {
// Make sure that GOAWAY frame is sent before dumping framer
state.framer.goawayFrame({
lastId: params.lastId,
code: params.code,
extra: params.extra
}, finish)
} else {
finish()
}
}
Connection.prototype._onStreamDrain = function _onStreamDrain (error) {
var state = this._spdyState
state.debug('id=0 _onStreamDrain')
state.framer.dump()
state.framer.unpipe(this.socket)
state.framer.resume()
if (this.socket.destroySoon) {
this.socket.destroySoon()
}
this.emit('close', error)
}
Connection.prototype.end = function end (callback) {
var state = this._spdyState
if (callback) {
this.once('close', callback)
}
this._goaway({
lastId: state.stream.lastId.both,
code: 'OK',
send: true
})
}
Connection.prototype.destroyStreams = function destroyStreams (err) {
var state = this._spdyState
Object.keys(state.stream.map).forEach(function (id) {
var stream = state.stream.map[id]
stream.destroy()
if (err) {
stream.emit('error', err)
}
})
}
Connection.prototype.isServer = function isServer () {
return this._spdyState.isServer
}
Connection.prototype.getXForwardedFor = function getXForwardFor () {
return this._spdyState.xForward
}
Connection.prototype.sendXForwardedFor = function sendXForwardedFor (host) {
var state = this._spdyState
if (state.version !== null) {
state.framer.xForwardedFor({ host: host })
} else {
state.xForward = host
}
}
Connection.prototype.pushPromise = function pushPromise (parent, uri, callback) {
var state = this._spdyState
var stream = this._createStream({
request: false,
parent: parent,
method: uri.method,
path: uri.path,
host: uri.host,
priority: uri.priority,
headers: uri.headers,
readable: false
})
var err
// TODO(indutny): deduplicate this logic somehow
if (this._isGoaway(stream.id)) {
err = new Error('Can\'t send PUSH_PROMISE after GOAWAY')
process.nextTick(function () {
if (callback) {
callback(err)
} else {
stream.emit('error', err)
}
})
return stream
}
if (uri.push && !state.acceptPush) {
err = new Error(
'Can\'t send PUSH_PROMISE, other side won\'t accept it')
process.nextTick(function () {
if (callback) { callback(err) } else {
stream.emit('error', err)
}
})
return stream
}
stream._sendPush(uri.status, uri.response, function (err) {
if (!callback) {
if (err) {
stream.emit('error', err)
}
return
}
if (err) { return callback(err) }
callback(null, stream)
})
return stream
}
Connection.prototype.setTimeout = function setTimeout (delay, callback) {
var state = this._spdyState
state.timeout.set(delay, callback)
}

View file

@ -0,0 +1,188 @@
'use strict'
var transport = require('../spdy-transport')
var utils = transport.utils
var assert = require('assert')
var debug = require('debug')('spdy:priority')
function PriorityNode (tree, options) {
this.tree = tree
this.id = options.id
this.parent = options.parent
this.weight = options.weight
// To be calculated in `addChild`
this.priorityFrom = 0
this.priorityTo = 1
this.priority = 1
this.children = {
list: [],
weight: 0
}
if (this.parent !== null) {
this.parent.addChild(this)
}
}
function compareChildren (a, b) {
return a.weight === b.weight ? a.id - b.id : a.weight - b.weight
}
PriorityNode.prototype.toJSON = function toJSON () {
return {
parent: this.parent,
weight: this.weight,
exclusive: this.exclusive
}
}
PriorityNode.prototype.getPriority = function getPriority () {
return this.priority
}
PriorityNode.prototype.getPriorityRange = function getPriorityRange () {
return { from: this.priorityFrom, to: this.priorityTo }
}
PriorityNode.prototype.addChild = function addChild (child) {
child.parent = this
utils.binaryInsert(this.children.list, child, compareChildren)
this.children.weight += child.weight
this._updatePriority(this.priorityFrom, this.priorityTo)
}
PriorityNode.prototype.remove = function remove () {
assert(this.parent, 'Can\'t remove root node')
this.parent.removeChild(this)
this.tree._removeNode(this)
// Move all children to the parent
for (var i = 0; i < this.children.list.length; i++) {
this.parent.addChild(this.children.list[i])
}
}
PriorityNode.prototype.removeChild = function removeChild (child) {
this.children.weight -= child.weight
var index = utils.binarySearch(this.children.list, child, compareChildren)
if (index !== -1 && this.children.list.length >= index) {
this.children.list.splice(index, 1)
}
}
PriorityNode.prototype.removeChildren = function removeChildren () {
var children = this.children.list
this.children.list = []
this.children.weight = 0
return children
}
PriorityNode.prototype._updatePriority = function _updatePriority (from, to) {
this.priority = to - from
this.priorityFrom = from
this.priorityTo = to
var weight = 0
for (var i = 0; i < this.children.list.length; i++) {
var node = this.children.list[i]
var nextWeight = weight + node.weight
node._updatePriority(
from + this.priority * (weight / this.children.weight),
from + this.priority * (nextWeight / this.children.weight)
)
weight = nextWeight
}
}
function PriorityTree (options) {
this.map = {}
this.list = []
this.defaultWeight = options.defaultWeight || 16
this.count = 0
this.maxCount = options.maxCount
// Root
this.root = this.add({
id: 0,
parent: null,
weight: 1
})
}
module.exports = PriorityTree
PriorityTree.create = function create (options) {
return new PriorityTree(options)
}
PriorityTree.prototype.add = function add (options) {
if (options.id === options.parent) {
return this.addDefault(options.id)
}
var parent = options.parent === null ? null : this.map[options.parent]
if (parent === undefined) {
return this.addDefault(options.id)
}
debug('add node=%d parent=%d weight=%d exclusive=%d',
options.id,
options.parent === null ? -1 : options.parent,
options.weight || this.defaultWeight,
options.exclusive ? 1 : 0)
var children
if (options.exclusive) {
children = parent.removeChildren()
}
var node = new PriorityNode(this, {
id: options.id,
parent: parent,
weight: options.weight || this.defaultWeight
})
this.map[options.id] = node
if (options.exclusive) {
for (var i = 0; i < children.length; i++) {
node.addChild(children[i])
}
}
this.count++
if (this.count > this.maxCount) {
debug('hit maximum remove id=%d', this.list[0].id)
this.list.shift().remove()
}
// Root node is not subject to removal
if (node.parent !== null) {
this.list.push(node)
}
return node
}
// Only for testing, should use `node`'s methods
PriorityTree.prototype.get = function get (id) {
return this.map[id]
}
PriorityTree.prototype.addDefault = function addDefault (id) {
debug('creating default node')
return this.add({ id: id, parent: 0, weight: this.defaultWeight })
}
PriorityTree.prototype._removeNode = function _removeNode (node) {
delete this.map[node.id]
var index = utils.binarySearch(this.list, node, compareChildren)
this.list.splice(index, 1)
this.count--
}

View file

@ -0,0 +1,4 @@
exports.DEFAULT_METHOD = 'GET'
exports.DEFAULT_HOST = 'localhost'
exports.MAX_PRIORITY_STREAMS = 100
exports.DEFAULT_MAX_CHUNK = 8 * 1024

View file

@ -0,0 +1,58 @@
'use strict'
var util = require('util')
var transport = require('../../../spdy-transport')
var base = require('./')
var Scheduler = base.Scheduler
function Framer (options) {
Scheduler.call(this)
this.version = null
this.compress = null
this.window = options.window
this.timeout = options.timeout
// Wait for `enablePush`
this.pushEnabled = null
}
util.inherits(Framer, Scheduler)
module.exports = Framer
Framer.prototype.setVersion = function setVersion (version) {
this.version = version
this.emit('version')
}
Framer.prototype.setCompression = function setCompresion (pair) {
this.compress = new transport.utils.LockStream(pair.compress)
}
Framer.prototype.enablePush = function enablePush (enable) {
this.pushEnabled = enable
this.emit('_pushEnabled')
}
Framer.prototype._checkPush = function _checkPush (callback) {
if (this.pushEnabled === null) {
this.once('_pushEnabled', function () {
this._checkPush(callback)
})
return
}
var err = null
if (!this.pushEnabled) {
err = new Error('PUSH_PROMISE disabled by other side')
}
process.nextTick(function () {
return callback(err)
})
}
Framer.prototype._resetTimeout = function _resetTimeout () {
if (this.timeout) {
this.timeout.reset()
}
}

View file

@ -0,0 +1,7 @@
'use strict'
exports.utils = require('./utils')
exports.constants = require('./constants')
exports.Scheduler = require('./scheduler')
exports.Parser = require('./parser')
exports.Framer = require('./framer')

View file

@ -0,0 +1,106 @@
'use strict'
var transport = require('../../../spdy-transport')
var util = require('util')
var utils = require('./').utils
var OffsetBuffer = require('obuf')
var Transform = require('readable-stream').Transform
function Parser (options) {
Transform.call(this, {
readableObjectMode: true
})
this.buffer = new OffsetBuffer()
this.partial = false
this.waiting = 0
this.window = options.window
this.version = null
this.decompress = null
this.dead = false
}
module.exports = Parser
util.inherits(Parser, Transform)
Parser.prototype.error = utils.error
Parser.prototype.kill = function kill () {
this.dead = true
}
Parser.prototype._transform = function transform (data, encoding, cb) {
if (!this.dead) { this.buffer.push(data) }
this._consume(cb)
}
Parser.prototype._consume = function _consume (cb) {
var self = this
function next (err, frame) {
if (err) {
return cb(err)
}
if (Array.isArray(frame)) {
for (var i = 0; i < frame.length; i++) {
self.push(frame[i])
}
} else if (frame) {
self.push(frame)
}
// Consume more packets
if (!sync) {
return self._consume(cb)
}
process.nextTick(function () {
self._consume(cb)
})
}
if (this.dead) {
return cb()
}
if (this.buffer.size < this.waiting) {
// No data at all
if (this.buffer.size === 0) {
return cb()
}
// Partial DATA frame or something that we can process partially
if (this.partial) {
var partial = this.buffer.clone(this.buffer.size)
this.buffer.skip(partial.size)
this.waiting -= partial.size
this.executePartial(partial, next)
return
}
// We shall not do anything until we get all expected data
return cb()
}
var sync = true
var content = this.buffer.clone(this.waiting)
this.buffer.skip(this.waiting)
this.execute(content, next)
sync = false
}
Parser.prototype.setVersion = function setVersion (version) {
this.version = version
this.emit('version', version)
}
Parser.prototype.setCompression = function setCompresion (pair) {
this.decompress = new transport.utils.LockStream(pair.decompress)
}

View file

@ -0,0 +1,216 @@
'use strict'
var transport = require('../../../spdy-transport')
var utils = transport.utils
var assert = require('assert')
var util = require('util')
var debug = require('debug')('spdy:scheduler')
var Readable = require('readable-stream').Readable
/*
* We create following structure in `pending`:
* [ [ id = 0 ], [ id = 1 ], [ id = 2 ], [ id = 0 ] ]
* chunks chunks chunks chunks
* chunks chunks
* chunks
*
* Then on the `.tick()` pass we pick one chunks from each item and remove the
* item if it is empty:
*
* [ [ id = 0 ], [ id = 2 ] ]
* chunks chunks
* chunks
*
* Writing out: chunks for 0, chunks for 1, chunks for 2, chunks for 0
*
* This way data is interleaved between the different streams.
*/
function Scheduler (options) {
Readable.call(this)
// Pretty big window by default
this.window = 0.25
if (options && options.window) { this.window = options.window }
this.sync = []
this.list = []
this.count = 0
this.pendingTick = false
}
util.inherits(Scheduler, Readable)
module.exports = Scheduler
// Just for testing, really
Scheduler.create = function create (options) {
return new Scheduler(options)
}
function insertCompare (a, b) {
return a.priority === b.priority
? a.stream - b.stream
: b.priority - a.priority
}
Scheduler.prototype.schedule = function schedule (data) {
var priority = data.priority
var stream = data.stream
var chunks = data.chunks
// Synchronous frames should not be interleaved
if (priority === false) {
debug('queue sync', chunks)
this.sync.push(data)
this.count += chunks.length
this._read()
return
}
debug('queue async priority=%d stream=%d', priority, stream, chunks)
var item = new SchedulerItem(stream, priority)
var index = utils.binaryLookup(this.list, item, insertCompare)
// Push new item
if (index >= this.list.length || insertCompare(this.list[index], item) !== 0) {
this.list.splice(index, 0, item)
} else { // Coalesce
item = this.list[index]
}
item.push(data)
this.count += chunks.length
this._read()
}
Scheduler.prototype._read = function _read () {
if (this.count === 0) {
return
}
if (this.pendingTick) {
return
}
this.pendingTick = true
var self = this
process.nextTick(function () {
self.pendingTick = false
self.tick()
})
}
Scheduler.prototype.tick = function tick () {
// No luck for async frames
if (!this.tickSync()) { return false }
return this.tickAsync()
}
Scheduler.prototype.tickSync = function tickSync () {
// Empty sync queue first
var sync = this.sync
var res = true
this.sync = []
for (var i = 0; i < sync.length; i++) {
var item = sync[i]
debug('tick sync pending=%d', this.count, item.chunks)
for (var j = 0; j < item.chunks.length; j++) {
this.count--
// TODO: handle stream backoff properly
try {
res = this.push(item.chunks[j])
} catch (err) {
this.emit('error', err)
return false
}
}
debug('after tick sync pending=%d', this.count)
// TODO(indutny): figure out the way to invoke callback on actual write
if (item.callback) {
item.callback(null)
}
}
return res
}
Scheduler.prototype.tickAsync = function tickAsync () {
var res = true
var list = this.list
if (list.length === 0) {
return res
}
var startPriority = list[0].priority
for (var index = 0; list.length > 0; index++) {
// Loop index
index %= list.length
if (startPriority - list[index].priority > this.window) { index = 0 }
debug('tick async index=%d start=%d', index, startPriority)
var current = list[index]
var item = current.shift()
if (current.isEmpty()) {
list.splice(index, 1)
if (index === 0 && list.length > 0) {
startPriority = list[0].priority
}
index--
}
debug('tick async pending=%d', this.count, item.chunks)
for (var i = 0; i < item.chunks.length; i++) {
this.count--
// TODO: handle stream backoff properly
try {
res = this.push(item.chunks[i])
} catch (err) {
this.emit('error', err)
return false
}
}
debug('after tick pending=%d', this.count)
// TODO(indutny): figure out the way to invoke callback on actual write
if (item.callback) {
item.callback(null)
}
if (!res) { break }
}
return res
}
Scheduler.prototype.dump = function dump () {
this.tickSync()
// Write everything out
while (!this.tickAsync()) {
// Intentional no-op
}
assert.strictEqual(this.count, 0)
}
function SchedulerItem (stream, priority) {
this.stream = stream
this.priority = priority
this.queue = []
}
SchedulerItem.prototype.push = function push (chunks) {
this.queue.push(chunks)
}
SchedulerItem.prototype.shift = function shift () {
return this.queue.shift()
}
SchedulerItem.prototype.isEmpty = function isEmpty () {
return this.queue.length === 0
}

View file

@ -0,0 +1,94 @@
'use strict'
var utils = exports
var util = require('util')
function ProtocolError (code, message) {
this.code = code
this.message = message
}
util.inherits(ProtocolError, Error)
utils.ProtocolError = ProtocolError
utils.error = function error (code, message) {
return new ProtocolError(code, message)
}
utils.reverse = function reverse (object) {
var result = []
Object.keys(object).forEach(function (key) {
result[object[key] | 0] = key
})
return result
}
// weight [1, 36] <=> priority [0, 7]
// This way weight=16 is preserved and has priority=3
utils.weightToPriority = function weightToPriority (weight) {
return ((Math.min(35, (weight - 1)) / 35) * 7) | 0
}
utils.priorityToWeight = function priorityToWeight (priority) {
return (((priority / 7) * 35) | 0) + 1
}
// Copy-Paste from node
exports.addHeaderLine = function addHeaderLine (field, value, dest) {
field = field.toLowerCase()
if (/^:/.test(field)) {
dest[field] = value
return
}
switch (field) {
// Array headers:
case 'set-cookie':
if (dest[field] !== undefined) {
dest[field].push(value)
} else {
dest[field] = [ value ]
}
break
/* eslint-disable max-len */
// list is taken from:
/* eslint-enable max-len */
case 'content-type':
case 'content-length':
case 'user-agent':
case 'referer':
case 'host':
case 'authorization':
case 'proxy-authorization':
case 'if-modified-since':
case 'if-unmodified-since':
case 'from':
case 'location':
case 'max-forwards':
// drop duplicates
if (dest[field] === undefined) {
dest[field] = value
}
break
case 'cookie':
// make semicolon-separated list
if (dest[field] !== undefined) {
dest[field] += '; ' + value
} else {
dest[field] = value
}
break
default:
// make comma-separated list
if (dest[field] !== undefined) {
dest[field] += ', ' + value
} else {
dest[field] = value
}
}
}

View file

@ -0,0 +1,93 @@
'use strict'
var transport = require('../../../spdy-transport')
var base = transport.protocol.base
exports.PREFACE_SIZE = 24
exports.PREFACE = 'PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n'
exports.PREFACE_BUFFER = Buffer.from(exports.PREFACE)
exports.PING_OPAQUE_SIZE = 8
exports.FRAME_HEADER_SIZE = 9
exports.INITIAL_MAX_FRAME_SIZE = 16384
exports.ABSOLUTE_MAX_FRAME_SIZE = 16777215
exports.HEADER_TABLE_SIZE = 4096
exports.DEFAULT_MAX_HEADER_LIST_SIZE = 80 * 1024 // as in http_parser
exports.MAX_INITIAL_WINDOW_SIZE = 2147483647
exports.DEFAULT_WEIGHT = 16
exports.MAX_CONCURRENT_STREAMS = Infinity
exports.frameType = {
DATA: 0,
HEADERS: 1,
PRIORITY: 2,
RST_STREAM: 3,
SETTINGS: 4,
PUSH_PROMISE: 5,
PING: 6,
GOAWAY: 7,
WINDOW_UPDATE: 8,
CONTINUATION: 9,
// Custom
X_FORWARDED_FOR: 0xde
}
exports.flags = {
ACK: 0x01, // SETTINGS-only
END_STREAM: 0x01,
END_HEADERS: 0x04,
PADDED: 0x08,
PRIORITY: 0x20
}
exports.settings = {
SETTINGS_HEADER_TABLE_SIZE: 0x01,
SETTINGS_ENABLE_PUSH: 0x02,
SETTINGS_MAX_CONCURRENT_STREAMS: 0x03,
SETTINGS_INITIAL_WINDOW_SIZE: 0x04,
SETTINGS_MAX_FRAME_SIZE: 0x05,
SETTINGS_MAX_HEADER_LIST_SIZE: 0x06
}
exports.settingsIndex = [
null,
'header_table_size',
'enable_push',
'max_concurrent_streams',
'initial_window_size',
'max_frame_size',
'max_header_list_size'
]
exports.error = {
OK: 0,
NO_ERROR: 0,
PROTOCOL_ERROR: 1,
INTERNAL_ERROR: 2,
FLOW_CONTROL_ERROR: 3,
SETTINGS_TIMEOUT: 4,
STREAM_CLOSED: 5,
INVALID_STREAM: 5,
FRAME_SIZE_ERROR: 6,
REFUSED_STREAM: 7,
CANCEL: 8,
COMPRESSION_ERROR: 9,
CONNECT_ERROR: 10,
ENHANCE_YOUR_CALM: 11,
INADEQUATE_SECURITY: 12,
HTTP_1_1_REQUIRED: 13
}
exports.errorByCode = base.utils.reverse(exports.error)
exports.DEFAULT_WINDOW = 64 * 1024 - 1
exports.goaway = exports.error
exports.goawayByCode = Object.assign({}, exports.errorByCode)
exports.goawayByCode[0] = 'OK'

View file

@ -0,0 +1,542 @@
'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)
}

View file

@ -0,0 +1,34 @@
'use strict'
var constants = require('./').constants
var hpack = require('hpack.js')
function Pool () {
}
module.exports = Pool
Pool.create = function create () {
return new Pool()
}
Pool.prototype.get = function get (version) {
var options = {
table: {
maxSize: constants.HEADER_TABLE_SIZE
}
}
var compress = hpack.compressor.create(options)
var decompress = hpack.decompressor.create(options)
return {
version: version,
compress: compress,
decompress: decompress
}
}
Pool.prototype.put = function put () {
}

View file

@ -0,0 +1,8 @@
'use strict'
exports.name = 'h2'
exports.constants = require('./constants')
exports.parser = require('./parser')
exports.framer = require('./framer')
exports.compressionPool = require('./hpack-pool')

View file

@ -0,0 +1,578 @@
'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()
})
}

View file

@ -0,0 +1,146 @@
'use strict'
var transport = require('../../../spdy-transport')
var base = transport.protocol.base
exports.FRAME_HEADER_SIZE = 8
exports.PING_OPAQUE_SIZE = 4
exports.MAX_CONCURRENT_STREAMS = Infinity
exports.DEFAULT_MAX_HEADER_LIST_SIZE = Infinity
exports.DEFAULT_WEIGHT = 16
exports.frameType = {
SYN_STREAM: 1,
SYN_REPLY: 2,
RST_STREAM: 3,
SETTINGS: 4,
PING: 6,
GOAWAY: 7,
HEADERS: 8,
WINDOW_UPDATE: 9,
// Custom
X_FORWARDED_FOR: 0xf000
}
exports.flags = {
FLAG_FIN: 0x01,
FLAG_COMPRESSED: 0x02,
FLAG_UNIDIRECTIONAL: 0x02
}
exports.error = {
PROTOCOL_ERROR: 1,
INVALID_STREAM: 2,
REFUSED_STREAM: 3,
UNSUPPORTED_VERSION: 4,
CANCEL: 5,
INTERNAL_ERROR: 6,
FLOW_CONTROL_ERROR: 7,
STREAM_IN_USE: 8,
// STREAM_ALREADY_CLOSED: 9
STREAM_CLOSED: 9,
INVALID_CREDENTIALS: 10,
FRAME_TOO_LARGE: 11
}
exports.errorByCode = base.utils.reverse(exports.error)
exports.settings = {
FLAG_SETTINGS_PERSIST_VALUE: 1,
FLAG_SETTINGS_PERSISTED: 2,
SETTINGS_UPLOAD_BANDWIDTH: 1,
SETTINGS_DOWNLOAD_BANDWIDTH: 2,
SETTINGS_ROUND_TRIP_TIME: 3,
SETTINGS_MAX_CONCURRENT_STREAMS: 4,
SETTINGS_CURRENT_CWND: 5,
SETTINGS_DOWNLOAD_RETRANS_RATE: 6,
SETTINGS_INITIAL_WINDOW_SIZE: 7,
SETTINGS_CLIENT_CERTIFICATE_VECTOR_SIZE: 8
}
exports.settingsIndex = [
null,
'upload_bandwidth',
'download_bandwidth',
'round_trip_time',
'max_concurrent_streams',
'current_cwnd',
'download_retrans_rate',
'initial_window_size',
'client_certificate_vector_size'
]
exports.DEFAULT_WINDOW = 64 * 1024
exports.MAX_INITIAL_WINDOW_SIZE = 2147483647
exports.goaway = {
OK: 0,
PROTOCOL_ERROR: 1,
INTERNAL_ERROR: 2
}
exports.goawayByCode = base.utils.reverse(exports.goaway)
exports.statusReason = {
100: 'Continue',
101: 'Switching Protocols',
102: 'Processing', // RFC 2518, obsoleted by RFC 4918
200: 'OK',
201: 'Created',
202: 'Accepted',
203: 'Non-Authoritative Information',
204: 'No Content',
205: 'Reset Content',
206: 'Partial Content',
207: 'Multi-Status', // RFC 4918
300: 'Multiple Choices',
301: 'Moved Permanently',
302: 'Moved Temporarily',
303: 'See Other',
304: 'Not Modified',
305: 'Use Proxy',
307: 'Temporary Redirect',
308: 'Permanent Redirect', // RFC 7238
400: 'Bad Request',
401: 'Unauthorized',
402: 'Payment Required',
403: 'Forbidden',
404: 'Not Found',
405: 'Method Not Allowed',
406: 'Not Acceptable',
407: 'Proxy Authentication Required',
408: 'Request Time-out',
409: 'Conflict',
410: 'Gone',
411: 'Length Required',
412: 'Precondition Failed',
413: 'Request Entity Too Large',
414: 'Request-URI Too Large',
415: 'Unsupported Media Type',
416: 'Requested Range Not Satisfiable',
417: 'Expectation Failed',
418: 'I\'m a teapot', // RFC 2324
422: 'Unprocessable Entity', // RFC 4918
423: 'Locked', // RFC 4918
424: 'Failed Dependency', // RFC 4918
425: 'Unordered Collection', // RFC 4918
426: 'Upgrade Required', // RFC 2817
428: 'Precondition Required', // RFC 6585
429: 'Too Many Requests', // RFC 6585
431: 'Request Header Fields Too Large', // RFC 6585
500: 'Internal Server Error',
501: 'Not Implemented',
502: 'Bad Gateway',
503: 'Service Unavailable',
504: 'Gateway Time-out',
505: 'HTTP Version Not Supported',
506: 'Variant Also Negotiates', // RFC 2295
507: 'Insufficient Storage', // RFC 4918
509: 'Bandwidth Limit Exceeded',
510: 'Not Extended', // RFC 2774
511: 'Network Authentication Required' // RFC 6585
}

View file

@ -0,0 +1,203 @@
'use strict'
var dictionary = {}
module.exports = dictionary
dictionary[2] = Buffer.from([
'optionsgetheadpostputdeletetraceacceptaccept-charsetaccept-encodingaccept-',
'languageauthorizationexpectfromhostif-modified-sinceif-matchif-none-matchi',
'f-rangeif-unmodifiedsincemax-forwardsproxy-authorizationrangerefererteuser',
'-agent10010120020120220320420520630030130230330430530630740040140240340440',
'5406407408409410411412413414415416417500501502503504505accept-rangesageeta',
'glocationproxy-authenticatepublicretry-afterservervarywarningwww-authentic',
'ateallowcontent-basecontent-encodingcache-controlconnectiondatetrailertran',
'sfer-encodingupgradeviawarningcontent-languagecontent-lengthcontent-locati',
'oncontent-md5content-rangecontent-typeetagexpireslast-modifiedset-cookieMo',
'ndayTuesdayWednesdayThursdayFridaySaturdaySundayJanFebMarAprMayJunJulAugSe',
'pOctNovDecchunkedtext/htmlimage/pngimage/jpgimage/gifapplication/xmlapplic',
'ation/xhtmltext/plainpublicmax-agecharset=iso-8859-1utf-8gzipdeflateHTTP/1',
'.1statusversionurl\x00'
].join(''))
dictionary[3] = Buffer.from([
0x00, 0x00, 0x00, 0x07, 0x6f, 0x70, 0x74, 0x69, // ....opti
0x6f, 0x6e, 0x73, 0x00, 0x00, 0x00, 0x04, 0x68, // ons....h
0x65, 0x61, 0x64, 0x00, 0x00, 0x00, 0x04, 0x70, // ead....p
0x6f, 0x73, 0x74, 0x00, 0x00, 0x00, 0x03, 0x70, // ost....p
0x75, 0x74, 0x00, 0x00, 0x00, 0x06, 0x64, 0x65, // ut....de
0x6c, 0x65, 0x74, 0x65, 0x00, 0x00, 0x00, 0x05, // lete....
0x74, 0x72, 0x61, 0x63, 0x65, 0x00, 0x00, 0x00, // trace...
0x06, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x00, // .accept.
0x00, 0x00, 0x0e, 0x61, 0x63, 0x63, 0x65, 0x70, // ...accep
0x74, 0x2d, 0x63, 0x68, 0x61, 0x72, 0x73, 0x65, // t-charse
0x74, 0x00, 0x00, 0x00, 0x0f, 0x61, 0x63, 0x63, // t....acc
0x65, 0x70, 0x74, 0x2d, 0x65, 0x6e, 0x63, 0x6f, // ept-enco
0x64, 0x69, 0x6e, 0x67, 0x00, 0x00, 0x00, 0x0f, // ding....
0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x2d, 0x6c, // accept-l
0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, 0x00, // anguage.
0x00, 0x00, 0x0d, 0x61, 0x63, 0x63, 0x65, 0x70, // ...accep
0x74, 0x2d, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x73, // t-ranges
0x00, 0x00, 0x00, 0x03, 0x61, 0x67, 0x65, 0x00, // ....age.
0x00, 0x00, 0x05, 0x61, 0x6c, 0x6c, 0x6f, 0x77, // ...allow
0x00, 0x00, 0x00, 0x0d, 0x61, 0x75, 0x74, 0x68, // ....auth
0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, // orizatio
0x6e, 0x00, 0x00, 0x00, 0x0d, 0x63, 0x61, 0x63, // n....cac
0x68, 0x65, 0x2d, 0x63, 0x6f, 0x6e, 0x74, 0x72, // he-contr
0x6f, 0x6c, 0x00, 0x00, 0x00, 0x0a, 0x63, 0x6f, // ol....co
0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, // nnection
0x00, 0x00, 0x00, 0x0c, 0x63, 0x6f, 0x6e, 0x74, // ....cont
0x65, 0x6e, 0x74, 0x2d, 0x62, 0x61, 0x73, 0x65, // ent-base
0x00, 0x00, 0x00, 0x10, 0x63, 0x6f, 0x6e, 0x74, // ....cont
0x65, 0x6e, 0x74, 0x2d, 0x65, 0x6e, 0x63, 0x6f, // ent-enco
0x64, 0x69, 0x6e, 0x67, 0x00, 0x00, 0x00, 0x10, // ding....
0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x2d, // content-
0x6c, 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, // language
0x00, 0x00, 0x00, 0x0e, 0x63, 0x6f, 0x6e, 0x74, // ....cont
0x65, 0x6e, 0x74, 0x2d, 0x6c, 0x65, 0x6e, 0x67, // ent-leng
0x74, 0x68, 0x00, 0x00, 0x00, 0x10, 0x63, 0x6f, // th....co
0x6e, 0x74, 0x65, 0x6e, 0x74, 0x2d, 0x6c, 0x6f, // ntent-lo
0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x00, // cation..
0x00, 0x0b, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, // ..conten
0x74, 0x2d, 0x6d, 0x64, 0x35, 0x00, 0x00, 0x00, // t-md5...
0x0d, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, // .content
0x2d, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x00, 0x00, // -range..
0x00, 0x0c, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, // ..conten
0x74, 0x2d, 0x74, 0x79, 0x70, 0x65, 0x00, 0x00, // t-type..
0x00, 0x04, 0x64, 0x61, 0x74, 0x65, 0x00, 0x00, // ..date..
0x00, 0x04, 0x65, 0x74, 0x61, 0x67, 0x00, 0x00, // ..etag..
0x00, 0x06, 0x65, 0x78, 0x70, 0x65, 0x63, 0x74, // ..expect
0x00, 0x00, 0x00, 0x07, 0x65, 0x78, 0x70, 0x69, // ....expi
0x72, 0x65, 0x73, 0x00, 0x00, 0x00, 0x04, 0x66, // res....f
0x72, 0x6f, 0x6d, 0x00, 0x00, 0x00, 0x04, 0x68, // rom....h
0x6f, 0x73, 0x74, 0x00, 0x00, 0x00, 0x08, 0x69, // ost....i
0x66, 0x2d, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x00, // f-match.
0x00, 0x00, 0x11, 0x69, 0x66, 0x2d, 0x6d, 0x6f, // ...if-mo
0x64, 0x69, 0x66, 0x69, 0x65, 0x64, 0x2d, 0x73, // dified-s
0x69, 0x6e, 0x63, 0x65, 0x00, 0x00, 0x00, 0x0d, // ince....
0x69, 0x66, 0x2d, 0x6e, 0x6f, 0x6e, 0x65, 0x2d, // if-none-
0x6d, 0x61, 0x74, 0x63, 0x68, 0x00, 0x00, 0x00, // match...
0x08, 0x69, 0x66, 0x2d, 0x72, 0x61, 0x6e, 0x67, // .if-rang
0x65, 0x00, 0x00, 0x00, 0x13, 0x69, 0x66, 0x2d, // e....if-
0x75, 0x6e, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69, // unmodifi
0x65, 0x64, 0x2d, 0x73, 0x69, 0x6e, 0x63, 0x65, // ed-since
0x00, 0x00, 0x00, 0x0d, 0x6c, 0x61, 0x73, 0x74, // ....last
0x2d, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, // -modifie
0x64, 0x00, 0x00, 0x00, 0x08, 0x6c, 0x6f, 0x63, // d....loc
0x61, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x00, 0x00, // ation...
0x0c, 0x6d, 0x61, 0x78, 0x2d, 0x66, 0x6f, 0x72, // .max-for
0x77, 0x61, 0x72, 0x64, 0x73, 0x00, 0x00, 0x00, // wards...
0x06, 0x70, 0x72, 0x61, 0x67, 0x6d, 0x61, 0x00, // .pragma.
0x00, 0x00, 0x12, 0x70, 0x72, 0x6f, 0x78, 0x79, // ...proxy
0x2d, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, // -authent
0x69, 0x63, 0x61, 0x74, 0x65, 0x00, 0x00, 0x00, // icate...
0x13, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2d, 0x61, // .proxy-a
0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, // uthoriza
0x74, 0x69, 0x6f, 0x6e, 0x00, 0x00, 0x00, 0x05, // tion....
0x72, 0x61, 0x6e, 0x67, 0x65, 0x00, 0x00, 0x00, // range...
0x07, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x72, // .referer
0x00, 0x00, 0x00, 0x0b, 0x72, 0x65, 0x74, 0x72, // ....retr
0x79, 0x2d, 0x61, 0x66, 0x74, 0x65, 0x72, 0x00, // y-after.
0x00, 0x00, 0x06, 0x73, 0x65, 0x72, 0x76, 0x65, // ...serve
0x72, 0x00, 0x00, 0x00, 0x02, 0x74, 0x65, 0x00, // r....te.
0x00, 0x00, 0x07, 0x74, 0x72, 0x61, 0x69, 0x6c, // ...trail
0x65, 0x72, 0x00, 0x00, 0x00, 0x11, 0x74, 0x72, // er....tr
0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x2d, 0x65, // ansfer-e
0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x00, // ncoding.
0x00, 0x00, 0x07, 0x75, 0x70, 0x67, 0x72, 0x61, // ...upgra
0x64, 0x65, 0x00, 0x00, 0x00, 0x0a, 0x75, 0x73, // de....us
0x65, 0x72, 0x2d, 0x61, 0x67, 0x65, 0x6e, 0x74, // er-agent
0x00, 0x00, 0x00, 0x04, 0x76, 0x61, 0x72, 0x79, // ....vary
0x00, 0x00, 0x00, 0x03, 0x76, 0x69, 0x61, 0x00, // ....via.
0x00, 0x00, 0x07, 0x77, 0x61, 0x72, 0x6e, 0x69, // ...warni
0x6e, 0x67, 0x00, 0x00, 0x00, 0x10, 0x77, 0x77, // ng....ww
0x77, 0x2d, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, // w-authen
0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x00, 0x00, // ticate..
0x00, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, // ..method
0x00, 0x00, 0x00, 0x03, 0x67, 0x65, 0x74, 0x00, // ....get.
0x00, 0x00, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, // ...statu
0x73, 0x00, 0x00, 0x00, 0x06, 0x32, 0x30, 0x30, // s....200
0x20, 0x4f, 0x4b, 0x00, 0x00, 0x00, 0x07, 0x76, // .OK....v
0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x00, 0x00, // ersion..
0x00, 0x08, 0x48, 0x54, 0x54, 0x50, 0x2f, 0x31, // ..HTTP.1
0x2e, 0x31, 0x00, 0x00, 0x00, 0x03, 0x75, 0x72, // .1....ur
0x6c, 0x00, 0x00, 0x00, 0x06, 0x70, 0x75, 0x62, // l....pub
0x6c, 0x69, 0x63, 0x00, 0x00, 0x00, 0x0a, 0x73, // lic....s
0x65, 0x74, 0x2d, 0x63, 0x6f, 0x6f, 0x6b, 0x69, // et-cooki
0x65, 0x00, 0x00, 0x00, 0x0a, 0x6b, 0x65, 0x65, // e....kee
0x70, 0x2d, 0x61, 0x6c, 0x69, 0x76, 0x65, 0x00, // p-alive.
0x00, 0x00, 0x06, 0x6f, 0x72, 0x69, 0x67, 0x69, // ...origi
0x6e, 0x31, 0x30, 0x30, 0x31, 0x30, 0x31, 0x32, // n1001012
0x30, 0x31, 0x32, 0x30, 0x32, 0x32, 0x30, 0x35, // 01202205
0x32, 0x30, 0x36, 0x33, 0x30, 0x30, 0x33, 0x30, // 20630030
0x32, 0x33, 0x30, 0x33, 0x33, 0x30, 0x34, 0x33, // 23033043
0x30, 0x35, 0x33, 0x30, 0x36, 0x33, 0x30, 0x37, // 05306307
0x34, 0x30, 0x32, 0x34, 0x30, 0x35, 0x34, 0x30, // 40240540
0x36, 0x34, 0x30, 0x37, 0x34, 0x30, 0x38, 0x34, // 64074084
0x30, 0x39, 0x34, 0x31, 0x30, 0x34, 0x31, 0x31, // 09410411
0x34, 0x31, 0x32, 0x34, 0x31, 0x33, 0x34, 0x31, // 41241341
0x34, 0x34, 0x31, 0x35, 0x34, 0x31, 0x36, 0x34, // 44154164
0x31, 0x37, 0x35, 0x30, 0x32, 0x35, 0x30, 0x34, // 17502504
0x35, 0x30, 0x35, 0x32, 0x30, 0x33, 0x20, 0x4e, // 505203.N
0x6f, 0x6e, 0x2d, 0x41, 0x75, 0x74, 0x68, 0x6f, // on-Autho
0x72, 0x69, 0x74, 0x61, 0x74, 0x69, 0x76, 0x65, // ritative
0x20, 0x49, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, // .Informa
0x74, 0x69, 0x6f, 0x6e, 0x32, 0x30, 0x34, 0x20, // tion204.
0x4e, 0x6f, 0x20, 0x43, 0x6f, 0x6e, 0x74, 0x65, // No.Conte
0x6e, 0x74, 0x33, 0x30, 0x31, 0x20, 0x4d, 0x6f, // nt301.Mo
0x76, 0x65, 0x64, 0x20, 0x50, 0x65, 0x72, 0x6d, // ved.Perm
0x61, 0x6e, 0x65, 0x6e, 0x74, 0x6c, 0x79, 0x34, // anently4
0x30, 0x30, 0x20, 0x42, 0x61, 0x64, 0x20, 0x52, // 00.Bad.R
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x34, 0x30, // equest40
0x31, 0x20, 0x55, 0x6e, 0x61, 0x75, 0x74, 0x68, // 1.Unauth
0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x34, 0x30, // orized40
0x33, 0x20, 0x46, 0x6f, 0x72, 0x62, 0x69, 0x64, // 3.Forbid
0x64, 0x65, 0x6e, 0x34, 0x30, 0x34, 0x20, 0x4e, // den404.N
0x6f, 0x74, 0x20, 0x46, 0x6f, 0x75, 0x6e, 0x64, // ot.Found
0x35, 0x30, 0x30, 0x20, 0x49, 0x6e, 0x74, 0x65, // 500.Inte
0x72, 0x6e, 0x61, 0x6c, 0x20, 0x53, 0x65, 0x72, // rnal.Ser
0x76, 0x65, 0x72, 0x20, 0x45, 0x72, 0x72, 0x6f, // ver.Erro
0x72, 0x35, 0x30, 0x31, 0x20, 0x4e, 0x6f, 0x74, // r501.Not
0x20, 0x49, 0x6d, 0x70, 0x6c, 0x65, 0x6d, 0x65, // .Impleme
0x6e, 0x74, 0x65, 0x64, 0x35, 0x30, 0x33, 0x20, // nted503.
0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x20, // Service.
0x55, 0x6e, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, // Unavaila
0x62, 0x6c, 0x65, 0x4a, 0x61, 0x6e, 0x20, 0x46, // bleJan.F
0x65, 0x62, 0x20, 0x4d, 0x61, 0x72, 0x20, 0x41, // eb.Mar.A
0x70, 0x72, 0x20, 0x4d, 0x61, 0x79, 0x20, 0x4a, // pr.May.J
0x75, 0x6e, 0x20, 0x4a, 0x75, 0x6c, 0x20, 0x41, // un.Jul.A
0x75, 0x67, 0x20, 0x53, 0x65, 0x70, 0x74, 0x20, // ug.Sept.
0x4f, 0x63, 0x74, 0x20, 0x4e, 0x6f, 0x76, 0x20, // Oct.Nov.
0x44, 0x65, 0x63, 0x20, 0x30, 0x30, 0x3a, 0x30, // Dec.00.0
0x30, 0x3a, 0x30, 0x30, 0x20, 0x4d, 0x6f, 0x6e, // 0.00.Mon
0x2c, 0x20, 0x54, 0x75, 0x65, 0x2c, 0x20, 0x57, // ..Tue..W
0x65, 0x64, 0x2c, 0x20, 0x54, 0x68, 0x75, 0x2c, // ed..Thu.
0x20, 0x46, 0x72, 0x69, 0x2c, 0x20, 0x53, 0x61, // .Fri..Sa
0x74, 0x2c, 0x20, 0x53, 0x75, 0x6e, 0x2c, 0x20, // t..Sun..
0x47, 0x4d, 0x54, 0x63, 0x68, 0x75, 0x6e, 0x6b, // GMTchunk
0x65, 0x64, 0x2c, 0x74, 0x65, 0x78, 0x74, 0x2f, // ed.text.
0x68, 0x74, 0x6d, 0x6c, 0x2c, 0x69, 0x6d, 0x61, // html.ima
0x67, 0x65, 0x2f, 0x70, 0x6e, 0x67, 0x2c, 0x69, // ge.png.i
0x6d, 0x61, 0x67, 0x65, 0x2f, 0x6a, 0x70, 0x67, // mage.jpg
0x2c, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x2f, 0x67, // .image.g
0x69, 0x66, 0x2c, 0x61, 0x70, 0x70, 0x6c, 0x69, // if.appli
0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x78, // cation.x
0x6d, 0x6c, 0x2c, 0x61, 0x70, 0x70, 0x6c, 0x69, // ml.appli
0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x78, // cation.x
0x68, 0x74, 0x6d, 0x6c, 0x2b, 0x78, 0x6d, 0x6c, // html.xml
0x2c, 0x74, 0x65, 0x78, 0x74, 0x2f, 0x70, 0x6c, // .text.pl
0x61, 0x69, 0x6e, 0x2c, 0x74, 0x65, 0x78, 0x74, // ain.text
0x2f, 0x6a, 0x61, 0x76, 0x61, 0x73, 0x63, 0x72, // .javascr
0x69, 0x70, 0x74, 0x2c, 0x70, 0x75, 0x62, 0x6c, // ipt.publ
0x69, 0x63, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, // icprivat
0x65, 0x6d, 0x61, 0x78, 0x2d, 0x61, 0x67, 0x65, // emax-age
0x3d, 0x67, 0x7a, 0x69, 0x70, 0x2c, 0x64, 0x65, // .gzip.de
0x66, 0x6c, 0x61, 0x74, 0x65, 0x2c, 0x73, 0x64, // flate.sd
0x63, 0x68, 0x63, 0x68, 0x61, 0x72, 0x73, 0x65, // chcharse
0x74, 0x3d, 0x75, 0x74, 0x66, 0x2d, 0x38, 0x63, // t.utf-8c
0x68, 0x61, 0x72, 0x73, 0x65, 0x74, 0x3d, 0x69, // harset.i
0x73, 0x6f, 0x2d, 0x38, 0x38, 0x35, 0x39, 0x2d, // so-8859-
0x31, 0x2c, 0x75, 0x74, 0x66, 0x2d, 0x2c, 0x2a, // 1.utf-..
0x2c, 0x65, 0x6e, 0x71, 0x3d, 0x30, 0x2e // .enq.0.
])
dictionary[3.1] = dictionary[3]

View file

@ -0,0 +1,519 @@
'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)
}

View file

@ -0,0 +1,9 @@
'use strict'
exports.name = 'spdy'
exports.dictionary = require('./dictionary')
exports.constants = require('./constants')
exports.parser = require('./parser')
exports.framer = require('./framer')
exports.compressionPool = require('./zlib-pool')

View file

@ -0,0 +1,485 @@
'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()
})
}

View file

@ -0,0 +1,65 @@
'use strict'
var zlibpool = exports
var zlib = require('zlib')
var transport = require('../../../spdy-transport')
// TODO(indutny): think about it, why has it always been Z_SYNC_FLUSH here.
// It should be possible to manually flush stuff after the write instead
function createDeflate (version, compression) {
var deflate = zlib.createDeflate({
dictionary: transport.protocol.spdy.dictionary[version],
flush: zlib.Z_SYNC_FLUSH,
windowBits: 11,
level: compression ? zlib.Z_DEFAULT_COMPRESSION : zlib.Z_NO_COMPRESSION
})
// For node.js v0.8
deflate._flush = zlib.Z_SYNC_FLUSH
return deflate
}
function createInflate (version) {
var inflate = zlib.createInflate({
dictionary: transport.protocol.spdy.dictionary[version],
flush: zlib.Z_SYNC_FLUSH
})
// For node.js v0.8
inflate._flush = zlib.Z_SYNC_FLUSH
return inflate
}
function Pool (compression) {
this.compression = compression
this.pool = {
2: [],
3: [],
3.1: []
}
}
zlibpool.create = function create (compression) {
return new Pool(compression)
}
Pool.prototype.get = function get (version) {
if (this.pool[version].length > 0) {
return this.pool[version].pop()
} else {
var id = version
return {
version: version,
compress: createDeflate(id, this.compression),
decompress: createInflate(id)
}
}
}
Pool.prototype.put = function put (pair) {
this.pool[pair.version].push(pair)
}

View file

@ -0,0 +1,710 @@
'use strict'
var transport = require('../spdy-transport')
var assert = require('assert')
var util = require('util')
var debug = {
client: require('debug')('spdy:stream:client'),
server: require('debug')('spdy:stream:server')
}
var Duplex = require('readable-stream').Duplex
function Stream (connection, options) {
Duplex.call(this)
var connectionState = connection._spdyState
var state = {}
this._spdyState = state
this.id = options.id
this.method = options.method
this.path = options.path
this.host = options.host
this.headers = options.headers || {}
this.connection = connection
this.parent = options.parent || null
state.socket = null
state.protocol = connectionState.protocol
state.constants = state.protocol.constants
// See _initPriority()
state.priority = null
state.version = this.connection.getVersion()
state.isServer = this.connection.isServer()
state.debug = state.isServer ? debug.server : debug.client
state.framer = connectionState.framer
state.parser = connectionState.parser
state.request = options.request
state.needResponse = options.request
state.window = connectionState.streamWindow.clone(options.id)
state.sessionWindow = connectionState.window
state.maxChunk = connectionState.maxChunk
// Can't send incoming request
// (See `.send()` method)
state.sent = !state.request
state.readable = options.readable !== false
state.writable = options.writable !== false
state.aborted = false
state.corked = 0
state.corkQueue = []
state.timeout = new transport.utils.Timeout(this)
this.on('finish', this._onFinish)
this.on('end', this._onEnd)
var self = this
function _onWindowOverflow () {
self._onWindowOverflow()
}
state.window.recv.on('overflow', _onWindowOverflow)
state.window.send.on('overflow', _onWindowOverflow)
this._initPriority(options.priority)
if (!state.readable) { this.push(null) }
if (!state.writable) {
this._writableState.ended = true
this._writableState.finished = true
}
}
util.inherits(Stream, Duplex)
exports.Stream = Stream
Stream.prototype._init = function _init (socket) {
this.socket = socket
}
Stream.prototype._initPriority = function _initPriority (priority) {
var state = this._spdyState
var connectionState = this.connection._spdyState
var root = connectionState.priorityRoot
if (!priority) {
state.priority = root.addDefault(this.id)
return
}
state.priority = root.add({
id: this.id,
parent: priority.parent,
weight: priority.weight,
exclusive: priority.exclusive
})
}
Stream.prototype._handleFrame = function _handleFrame (frame) {
var state = this._spdyState
// Ignore any kind of data after abort
if (state.aborted) {
state.debug('id=%d ignoring frame=%s after abort', this.id, frame.type)
return
}
// Restart the timer on incoming frames
state.timeout.reset()
if (frame.type === 'DATA') {
this._handleData(frame)
} else if (frame.type === 'HEADERS') {
this._handleHeaders(frame)
} else if (frame.type === 'RST') {
this._handleRST(frame)
} else if (frame.type === 'WINDOW_UPDATE') { this._handleWindowUpdate(frame) } else if (frame.type === 'PRIORITY') {
this._handlePriority(frame)
} else if (frame.type === 'PUSH_PROMISE') { this._handlePushPromise(frame) }
if (frame.fin) {
state.debug('id=%d end', this.id)
this.push(null)
}
}
function checkAborted (stream, state, callback) {
if (state.aborted) {
state.debug('id=%d abort write', stream.id)
process.nextTick(function () {
callback(new Error('Stream write aborted'))
})
return true
}
return false
}
function _send (stream, state, data, callback) {
if (checkAborted(stream, state, callback)) {
return
}
state.debug('id=%d presend=%d', stream.id, data.length)
state.timeout.reset()
state.window.send.update(-data.length, function () {
if (checkAborted(stream, state, callback)) {
return
}
state.debug('id=%d send=%d', stream.id, data.length)
state.timeout.reset()
state.framer.dataFrame({
id: stream.id,
priority: state.priority.getPriority(),
fin: false,
data: data
}, function (err) {
state.debug('id=%d postsend=%d', stream.id, data.length)
callback(err)
})
})
}
Stream.prototype._write = function _write (data, enc, callback) {
var state = this._spdyState
// Send the request if it wasn't sent
if (!state.sent) { this.send() }
// Writes should come after pending control frames (response and headers)
if (state.corked !== 0) {
var self = this
state.corkQueue.push(function () {
self._write(data, enc, callback)
})
return
}
// Split DATA in chunks to prevent window from going negative
this._splitStart(data, _send, callback)
}
Stream.prototype._splitStart = function _splitStart (data, onChunk, callback) {
return this._split(data, 0, onChunk, callback)
}
Stream.prototype._split = function _split (data, offset, onChunk, callback) {
if (offset === data.length) {
return process.nextTick(callback)
}
var state = this._spdyState
var local = state.window.send
var session = state.sessionWindow.send
var availSession = Math.max(0, session.getCurrent())
if (availSession === 0) {
availSession = session.getMax()
}
var availLocal = Math.max(0, local.getCurrent())
if (availLocal === 0) {
availLocal = local.getMax()
}
var avail = Math.min(availSession, availLocal)
avail = Math.min(avail, state.maxChunk)
var self = this
if (avail === 0) {
state.window.send.update(0, function () {
self._split(data, offset, onChunk, callback)
})
return
}
// Split data in chunks in a following way:
var limit = avail
var size = Math.min(data.length - offset, limit)
var chunk = data.slice(offset, offset + size)
onChunk(this, state, chunk, function (err) {
if (err) { return callback(err) }
// Get the next chunk
self._split(data, offset + size, onChunk, callback)
})
}
Stream.prototype._read = function _read () {
var state = this._spdyState
if (!state.window.recv.isDraining()) {
return
}
var delta = state.window.recv.getDelta()
state.debug('id=%d window emptying, update by %d', this.id, delta)
state.window.recv.update(delta)
state.framer.windowUpdateFrame({
id: this.id,
delta: delta
})
}
Stream.prototype._handleData = function _handleData (frame) {
var state = this._spdyState
// DATA on ended or not readable stream!
if (!state.readable || this._readableState.ended) {
state.framer.rstFrame({ id: this.id, code: 'STREAM_CLOSED' })
return
}
state.debug('id=%d recv=%d', this.id, frame.data.length)
state.window.recv.update(-frame.data.length)
this.push(frame.data)
}
Stream.prototype._handleRST = function _handleRST (frame) {
if (frame.code !== 'CANCEL') {
this.emit('error', new Error('Got RST: ' + frame.code))
}
this.abort()
}
Stream.prototype._handleWindowUpdate = function _handleWindowUpdate (frame) {
var state = this._spdyState
state.window.send.update(frame.delta)
}
Stream.prototype._onWindowOverflow = function _onWindowOverflow () {
var state = this._spdyState
state.debug('id=%d window overflow', this.id)
state.framer.rstFrame({ id: this.id, code: 'FLOW_CONTROL_ERROR' })
this.aborted = true
this.emit('error', new Error('HTTP2 window overflow'))
}
Stream.prototype._handlePriority = function _handlePriority (frame) {
var state = this._spdyState
state.priority.remove()
state.priority = null
this._initPriority(frame.priority)
// Mostly for testing purposes
this.emit('priority', frame.priority)
}
Stream.prototype._handleHeaders = function _handleHeaders (frame) {
var state = this._spdyState
if (!state.readable || this._readableState.ended) {
state.framer.rstFrame({ id: this.id, code: 'STREAM_CLOSED' })
return
}
if (state.needResponse) {
return this._handleResponse(frame)
}
this.emit('headers', frame.headers)
}
Stream.prototype._handleResponse = function _handleResponse (frame) {
var state = this._spdyState
if (frame.headers[':status'] === undefined) {
state.framer.rstFrame({ id: this.id, code: 'PROTOCOL_ERROR' })
return
}
state.needResponse = false
this.emit('response', frame.headers[':status'] | 0, frame.headers)
}
Stream.prototype._onFinish = function _onFinish () {
var state = this._spdyState
// Send the request if it wasn't sent
if (!state.sent) {
// NOTE: will send HEADERS with FIN flag
this.send()
} else {
// Just an `.end()` without any writes will trigger immediate `finish` event
// without any calls to `_write()`.
if (state.corked !== 0) {
var self = this
state.corkQueue.push(function () {
self._onFinish()
})
return
}
state.framer.dataFrame({
id: this.id,
priority: state.priority.getPriority(),
fin: true,
data: Buffer.alloc(0)
})
}
this._maybeClose()
}
Stream.prototype._onEnd = function _onEnd () {
this._maybeClose()
}
Stream.prototype._checkEnded = function _checkEnded (callback) {
var state = this._spdyState
var ended = false
if (state.aborted) { ended = true }
if (!state.writable || this._writableState.finished) { ended = true }
if (!ended) {
return true
}
if (!callback) {
return false
}
var err = new Error('Ended stream can\'t send frames')
process.nextTick(function () {
callback(err)
})
return false
}
Stream.prototype._maybeClose = function _maybeClose () {
var state = this._spdyState
// .abort() emits `close`
if (state.aborted) {
return
}
if ((!state.readable || this._readableState.ended) &&
this._writableState.finished) {
// Clear timeout
state.timeout.set(0)
this.emit('close')
}
}
Stream.prototype._handlePushPromise = function _handlePushPromise (frame) {
var push = this.connection._createStream({
id: frame.promisedId,
parent: this,
push: true,
request: true,
method: frame.headers[':method'],
path: frame.headers[':path'],
host: frame.headers[':authority'],
priority: frame.priority,
headers: frame.headers,
writable: false
})
// GOAWAY
if (this.connection._isGoaway(push.id)) {
return
}
if (!this.emit('pushPromise', push)) {
push.abort()
}
}
Stream.prototype._hardCork = function _hardCork () {
var state = this._spdyState
this.cork()
state.corked++
}
Stream.prototype._hardUncork = function _hardUncork () {
var state = this._spdyState
this.uncork()
state.corked--
if (state.corked !== 0) {
return
}
// Invoke callbacks
var queue = state.corkQueue
state.corkQueue = []
for (var i = 0; i < queue.length; i++) {
queue[i]()
}
}
Stream.prototype._sendPush = function _sendPush (status, response, callback) {
var self = this
var state = this._spdyState
this._hardCork()
state.framer.pushFrame({
id: this.parent.id,
promisedId: this.id,
priority: state.priority.toJSON(),
path: this.path,
host: this.host,
method: this.method,
status: status,
headers: this.headers,
response: response
}, function (err) {
self._hardUncork()
callback(err)
})
}
Stream.prototype._wasSent = function _wasSent () {
var state = this._spdyState
return state.sent
}
// Public API
Stream.prototype.send = function send (callback) {
var state = this._spdyState
if (state.sent) {
var err = new Error('Stream was already sent')
process.nextTick(function () {
if (callback) {
callback(err)
}
})
return
}
state.sent = true
state.timeout.reset()
// GET requests should always be auto-finished
if (this.method === 'GET') {
this._writableState.ended = true
this._writableState.finished = true
}
// TODO(indunty): ideally it should just take a stream object as an input
var self = this
this._hardCork()
state.framer.requestFrame({
id: this.id,
method: this.method,
path: this.path,
host: this.host,
priority: state.priority.toJSON(),
headers: this.headers,
fin: this._writableState.finished
}, function (err) {
self._hardUncork()
if (!callback) {
return
}
callback(err)
})
}
Stream.prototype.respond = function respond (status, headers, callback) {
var self = this
var state = this._spdyState
assert(!state.request, 'Can\'t respond on request')
state.timeout.reset()
if (!this._checkEnded(callback)) { return }
var frame = {
id: this.id,
status: status,
headers: headers
}
this._hardCork()
state.framer.responseFrame(frame, function (err) {
self._hardUncork()
if (callback) { callback(err) }
})
}
Stream.prototype.setWindow = function setWindow (size) {
var state = this._spdyState
state.timeout.reset()
if (!this._checkEnded()) {
return
}
state.debug('id=%d force window max=%d', this.id, size)
state.window.recv.setMax(size)
var delta = state.window.recv.getDelta()
if (delta === 0) { return }
state.framer.windowUpdateFrame({
id: this.id,
delta: delta
})
state.window.recv.update(delta)
}
Stream.prototype.sendHeaders = function sendHeaders (headers, callback) {
var self = this
var state = this._spdyState
state.timeout.reset()
if (!this._checkEnded(callback)) {
return
}
// Request wasn't yet send, coalesce headers
if (!state.sent) {
this.headers = Object.assign({}, this.headers)
Object.assign(this.headers, headers)
process.nextTick(function () {
if (callback) {
callback(null)
}
})
return
}
this._hardCork()
state.framer.headersFrame({
id: this.id,
headers: headers
}, function (err) {
self._hardUncork()
if (callback) { callback(err) }
})
}
Stream.prototype._destroy = function destroy () {
this.abort()
}
Stream.prototype.abort = function abort (code, callback) {
var state = this._spdyState
// .abort(callback)
if (typeof code === 'function') {
callback = code
code = null
}
if (this._readableState.ended && this._writableState.finished) {
state.debug('id=%d already closed', this.id)
if (callback) {
process.nextTick(callback)
}
return
}
if (state.aborted) {
state.debug('id=%d already aborted', this.id)
if (callback) { process.nextTick(callback) }
return
}
state.aborted = true
state.debug('id=%d abort', this.id)
this.setTimeout(0)
var abortCode = code || 'CANCEL'
state.framer.rstFrame({
id: this.id,
code: abortCode
})
var self = this
process.nextTick(function () {
if (callback) {
callback(null)
}
self.emit('close', new Error('Aborted, code: ' + abortCode))
})
}
Stream.prototype.setPriority = function setPriority (info) {
var state = this._spdyState
state.timeout.reset()
if (!this._checkEnded()) {
return
}
state.debug('id=%d priority change', this.id, info)
var frame = { id: this.id, priority: info }
// Change priority on this side
this._handlePriority(frame)
// And on the other too
state.framer.priorityFrame(frame)
}
Stream.prototype.pushPromise = function pushPromise (uri, callback) {
if (!this._checkEnded(callback)) {
return
}
var self = this
this._hardCork()
var push = this.connection.pushPromise(this, uri, function (err) {
self._hardUncork()
if (!err) {
push._hardUncork()
}
if (callback) {
return callback(err, push)
}
if (err) { push.emit('error', err) }
})
push._hardCork()
return push
}
Stream.prototype.setMaxChunk = function setMaxChunk (size) {
var state = this._spdyState
state.maxChunk = size
}
Stream.prototype.setTimeout = function setTimeout (delay, callback) {
var state = this._spdyState
state.timeout.set(delay, callback)
}

View file

@ -0,0 +1,196 @@
'use strict'
var util = require('util')
var isNode = require('detect-node')
// Node.js 0.8, 0.10 and 0.12 support
Object.assign = (process.versions.modules >= 46 || !isNode)
? Object.assign // eslint-disable-next-line
: util._extend
function QueueItem () {
this.prev = null
this.next = null
}
exports.QueueItem = QueueItem
function Queue () {
QueueItem.call(this)
this.prev = this
this.next = this
}
util.inherits(Queue, QueueItem)
exports.Queue = Queue
Queue.prototype.insertTail = function insertTail (item) {
item.prev = this.prev
item.next = this
item.prev.next = item
item.next.prev = item
}
Queue.prototype.remove = function remove (item) {
var next = item.next
var prev = item.prev
item.next = item
item.prev = item
next.prev = prev
prev.next = next
}
Queue.prototype.head = function head () {
return this.next
}
Queue.prototype.tail = function tail () {
return this.prev
}
Queue.prototype.isEmpty = function isEmpty () {
return this.next === this
}
Queue.prototype.isRoot = function isRoot (item) {
return this === item
}
function LockStream (stream) {
this.locked = false
this.queue = []
this.stream = stream
}
exports.LockStream = LockStream
LockStream.prototype.write = function write (chunks, callback) {
var self = this
// Do not let it interleave
if (this.locked) {
this.queue.push(function () {
return self.write(chunks, callback)
})
return
}
this.locked = true
function done (err, chunks) {
self.stream.removeListener('error', done)
self.locked = false
if (self.queue.length > 0) { self.queue.shift()() }
callback(err, chunks)
}
this.stream.on('error', done)
// Accumulate all output data
var output = []
function onData (chunk) {
output.push(chunk)
}
this.stream.on('data', onData)
function next (err) {
self.stream.removeListener('data', onData)
if (err) {
return done(err)
}
done(null, output)
}
for (var i = 0; i < chunks.length - 1; i++) { this.stream.write(chunks[i]) }
if (chunks.length > 0) {
this.stream.write(chunks[i], next)
} else { process.nextTick(next) }
if (this.stream.execute) {
this.stream.execute(function (err) {
if (err) { return done(err) }
})
}
}
// Just finds the place in array to insert
function binaryLookup (list, item, compare) {
var start = 0
var end = list.length
while (start < end) {
var pos = (start + end) >> 1
var cmp = compare(item, list[pos])
if (cmp === 0) {
start = pos
end = pos
break
} else if (cmp < 0) {
end = pos
} else {
start = pos + 1
}
}
return start
}
exports.binaryLookup = binaryLookup
function binaryInsert (list, item, compare) {
var index = binaryLookup(list, item, compare)
list.splice(index, 0, item)
}
exports.binaryInsert = binaryInsert
function binarySearch (list, item, compare) {
var index = binaryLookup(list, item, compare)
if (index >= list.length) {
return -1
}
if (compare(item, list[index]) === 0) {
return index
}
return -1
}
exports.binarySearch = binarySearch
function Timeout (object) {
this.delay = 0
this.timer = null
this.object = object
}
exports.Timeout = Timeout
Timeout.prototype.set = function set (delay, callback) {
this.delay = delay
this.reset()
if (!callback) { return }
if (this.delay === 0) {
this.object.removeListener('timeout', callback)
} else {
this.object.once('timeout', callback)
}
}
Timeout.prototype.reset = function reset () {
if (this.timer !== null) {
clearTimeout(this.timer)
this.timer = null
}
if (this.delay === 0) { return }
var self = this
this.timer = setTimeout(function () {
self.timer = null
self.object.emit('timeout')
}, this.delay)
}

View file

@ -0,0 +1,174 @@
'use strict'
var util = require('util')
var EventEmitter = require('events').EventEmitter
var debug = {
server: require('debug')('spdy:window:server'),
client: require('debug')('spdy:window:client')
}
function Side (window, name, options) {
EventEmitter.call(this)
this.name = name
this.window = window
this.current = options.size
this.max = options.size
this.limit = options.max
this.lowWaterMark = options.lowWaterMark === undefined
? this.max / 2
: options.lowWaterMark
this._refilling = false
this._refillQueue = []
}
util.inherits(Side, EventEmitter)
Side.prototype.setMax = function setMax (max) {
this.window.debug('id=%d side=%s setMax=%d',
this.window.id,
this.name,
max)
this.max = max
this.lowWaterMark = this.max / 2
}
Side.prototype.updateMax = function updateMax (max) {
var delta = max - this.max
this.window.debug('id=%d side=%s updateMax=%d delta=%d',
this.window.id,
this.name,
max,
delta)
this.max = max
this.lowWaterMark = max / 2
this.update(delta)
}
Side.prototype.setLowWaterMark = function setLowWaterMark (lwm) {
this.lowWaterMark = lwm
}
Side.prototype.update = function update (size, callback) {
// Not enough space for the update, wait for refill
if (size <= 0 && callback && this.isEmpty()) {
this.window.debug('id=%d side=%s wait for refill=%d [%d/%d]',
this.window.id,
this.name,
-size,
this.current,
this.max)
this._refillQueue.push({
size: size,
callback: callback
})
return
}
this.current += size
if (this.current > this.limit) {
this.emit('overflow')
return
}
this.window.debug('id=%d side=%s update by=%d [%d/%d]',
this.window.id,
this.name,
size,
this.current,
this.max)
// Time to send WINDOW_UPDATE
if (size < 0 && this.isDraining()) {
this.window.debug('id=%d side=%s drained', this.window.id, this.name)
this.emit('drain')
}
// Time to write
if (size > 0 && this.current > 0 && this.current <= size) {
this.window.debug('id=%d side=%s full', this.window.id, this.name)
this.emit('full')
}
this._processRefillQueue()
if (callback) { process.nextTick(callback) }
}
Side.prototype.getCurrent = function getCurrent () {
return this.current
}
Side.prototype.getMax = function getMax () {
return this.max
}
Side.prototype.getDelta = function getDelta () {
return this.max - this.current
}
Side.prototype.isDraining = function isDraining () {
return this.current <= this.lowWaterMark
}
Side.prototype.isEmpty = function isEmpty () {
return this.current <= 0
}
// Private
Side.prototype._processRefillQueue = function _processRefillQueue () {
// Prevent recursion
if (this._refilling) {
return
}
this._refilling = true
while (this._refillQueue.length > 0) {
var item = this._refillQueue[0]
if (this.isEmpty()) {
break
}
this.window.debug('id=%d side=%s refilled for size=%d',
this.window.id,
this.name,
-item.size)
this._refillQueue.shift()
this.update(item.size, item.callback)
}
this._refilling = false
}
function Window (options) {
this.id = options.id
this.isServer = options.isServer
this.debug = this.isServer ? debug.server : debug.client
this.recv = new Side(this, 'recv', options.recv)
this.send = new Side(this, 'send', options.send)
}
module.exports = Window
Window.prototype.clone = function clone (id) {
return new Window({
id: id,
isServer: this.isServer,
recv: {
size: this.recv.max,
max: this.recv.limit,
lowWaterMark: this.recv.lowWaterMark
},
send: {
size: this.send.max,
max: this.send.limit,
lowWaterMark: this.send.lowWaterMark
}
})
}