846 lines
20 KiB
JavaScript
846 lines
20 KiB
JavaScript
'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)
|
|
}
|