| 'use strict' |
| |
| var assert = require('assert') |
| var https = require('https') |
| var http = require('http') |
| var tls = require('tls') |
| var net = require('net') |
| var util = require('util') |
| var selectHose = require('select-hose') |
| var transport = require('spdy-transport') |
| var debug = require('debug')('spdy:server') |
| var EventEmitter = require('events').EventEmitter |
| |
| // Node.js 0.8, 0.10 and 0.12 support |
| Object.assign = process.versions.modules >= 46 |
| ? Object.assign // eslint-disable-next-line |
| : util._extend |
| |
| var spdy = require('../spdy') |
| |
| var proto = {} |
| |
| function instantiate (base) { |
| function Server (options, handler) { |
| this._init(base, options, handler) |
| } |
| util.inherits(Server, base) |
| |
| Server.create = function create (options, handler) { |
| return new Server(options, handler) |
| } |
| |
| Object.keys(proto).forEach(function (key) { |
| Server.prototype[key] = proto[key] |
| }) |
| |
| return Server |
| } |
| |
| proto._init = function _init (base, options, handler) { |
| var state = {} |
| this._spdyState = state |
| |
| state.options = options.spdy || {} |
| |
| var protocols = state.options.protocols || [ |
| 'h2', |
| 'spdy/3.1', 'spdy/3', 'spdy/2', |
| 'http/1.1', 'http/1.0' |
| ] |
| |
| var actualOptions = Object.assign({ |
| NPNProtocols: protocols, |
| |
| // Future-proof |
| ALPNProtocols: protocols |
| }, options) |
| |
| state.secure = this instanceof tls.Server |
| |
| if (state.secure) { |
| base.call(this, actualOptions) |
| } else { |
| base.call(this) |
| } |
| |
| // Support HEADERS+FIN |
| this.httpAllowHalfOpen = true |
| |
| var event = state.secure ? 'secureConnection' : 'connection' |
| |
| state.listeners = this.listeners(event).slice() |
| assert(state.listeners.length > 0, 'Server does not have default listeners') |
| this.removeAllListeners(event) |
| |
| if (state.options.plain) { |
| this.on(event, this._onPlainConnection) |
| } else { this.on(event, this._onConnection) } |
| |
| if (handler) { |
| this.on('request', handler) |
| } |
| |
| debug('server init secure=%d', state.secure) |
| } |
| |
| proto._onConnection = function _onConnection (socket) { |
| var state = this._spdyState |
| |
| var protocol |
| if (state.secure) { |
| protocol = socket.npnProtocol || socket.alpnProtocol |
| } |
| |
| this._handleConnection(socket, protocol) |
| } |
| |
| proto._handleConnection = function _handleConnection (socket, protocol) { |
| var state = this._spdyState |
| |
| if (!protocol) { |
| protocol = state.options.protocol |
| } |
| |
| debug('incoming socket protocol=%j', protocol) |
| |
| // No way we can do anything with the socket |
| if (!protocol || protocol === 'http/1.1' || protocol === 'http/1.0') { |
| debug('to default handler it goes') |
| return this._invokeDefault(socket) |
| } |
| |
| socket.setNoDelay(true) |
| |
| var connection = transport.connection.create(socket, Object.assign({ |
| protocol: /spdy/.test(protocol) ? 'spdy' : 'http2', |
| isServer: true |
| }, state.options.connection || {})) |
| |
| // Set version when we are certain |
| if (protocol === 'http2') { connection.start(4) } else if (protocol === 'spdy/3.1') { |
| connection.start(3.1) |
| } else if (protocol === 'spdy/3') { connection.start(3) } else if (protocol === 'spdy/2') { |
| connection.start(2) |
| } |
| |
| connection.on('error', function () { |
| socket.destroy() |
| }) |
| |
| var self = this |
| connection.on('stream', function (stream) { |
| self._onStream(stream) |
| }) |
| } |
| |
| // HTTP2 preface |
| var PREFACE = 'PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n' |
| var PREFACE_BUFFER = Buffer.from(PREFACE) |
| |
| function hoseFilter (data, callback) { |
| if (data.length < 1) { |
| return callback(null, null) |
| } |
| |
| // SPDY! |
| if (data[0] === 0x80) { return callback(null, 'spdy') } |
| |
| var avail = Math.min(data.length, PREFACE_BUFFER.length) |
| for (var i = 0; i < avail; i++) { |
| if (data[i] !== PREFACE_BUFFER[i]) { return callback(null, 'http/1.1') } |
| } |
| |
| // Not enough bytes to be sure about HTTP2 |
| if (avail !== PREFACE_BUFFER.length) { return callback(null, null) } |
| |
| return callback(null, 'h2') |
| } |
| |
| proto._onPlainConnection = function _onPlainConnection (socket) { |
| var hose = selectHose.create(socket, {}, hoseFilter) |
| |
| var self = this |
| hose.on('select', function (protocol, socket) { |
| self._handleConnection(socket, protocol) |
| }) |
| |
| hose.on('error', function (err) { |
| debug('hose error %j', err.message) |
| socket.destroy() |
| }) |
| } |
| |
| proto._invokeDefault = function _invokeDefault (socket) { |
| var state = this._spdyState |
| |
| for (var i = 0; i < state.listeners.length; i++) { state.listeners[i].call(this, socket) } |
| } |
| |
| proto._onStream = function _onStream (stream) { |
| var state = this._spdyState |
| |
| var handle = spdy.handle.create(this._spdyState.options, stream) |
| |
| var socketOptions = { |
| handle: handle, |
| allowHalfOpen: true |
| } |
| |
| var socket |
| if (state.secure) { |
| socket = new spdy.Socket(stream.connection.socket, socketOptions) |
| } else { |
| socket = new net.Socket(socketOptions) |
| } |
| |
| // This is needed because the `error` listener, added by the default |
| // `connection` listener, no longer has bound arguments. It relies instead |
| // on the `server` property of the socket. See https://github.com/nodejs/node/pull/11926 |
| // for more details. |
| // This is only done for Node.js >= 4 in order to not break compatibility |
| // with older versions of the platform. |
| if (process.versions.modules >= 46) { socket.server = this } |
| |
| handle.assignSocket(socket) |
| |
| // For v0.8 |
| socket.readable = true |
| socket.writable = true |
| |
| this._invokeDefault(socket) |
| |
| // For v0.8, 0.10 and 0.12 |
| if (process.versions.modules < 46) { |
| // eslint-disable-next-line |
| this.listenerCount = EventEmitter.listenerCount.bind(this) |
| } |
| |
| // Add lazy `checkContinue` listener, otherwise `res.writeContinue` will be |
| // called before the response object was patched by us. |
| if (stream.headers.expect !== undefined && |
| /100-continue/i.test(stream.headers.expect) && |
| this.listenerCount('checkContinue') === 0) { |
| this.once('checkContinue', function (req, res) { |
| res.writeContinue() |
| |
| this.emit('request', req, res) |
| }) |
| } |
| |
| handle.emitRequest() |
| } |
| |
| proto.emit = function emit (event, req, res) { |
| if (event !== 'request' && event !== 'checkContinue') { |
| return EventEmitter.prototype.emit.apply(this, arguments) |
| } |
| |
| if (!(req.socket._handle instanceof spdy.handle)) { |
| debug('not spdy req/res') |
| req.isSpdy = false |
| req.spdyVersion = 1 |
| res.isSpdy = false |
| res.spdyVersion = 1 |
| return EventEmitter.prototype.emit.apply(this, arguments) |
| } |
| |
| var handle = req.connection._handle |
| |
| req.isSpdy = true |
| req.spdyVersion = handle.getStream().connection.getVersion() |
| res.isSpdy = true |
| res.spdyVersion = req.spdyVersion |
| req.spdyStream = handle.getStream() |
| |
| debug('override req/res') |
| res.writeHead = spdy.response.writeHead |
| res.end = spdy.response.end |
| res.push = spdy.response.push |
| res.writeContinue = spdy.response.writeContinue |
| res.spdyStream = handle.getStream() |
| |
| res._req = req |
| |
| handle.assignRequest(req) |
| handle.assignResponse(res) |
| |
| return EventEmitter.prototype.emit.apply(this, arguments) |
| } |
| |
| exports.Server = instantiate(https.Server) |
| exports.PlainServer = instantiate(http.Server) |
| |
| exports.create = function create (base, options, handler) { |
| if (typeof base === 'object') { |
| handler = options |
| options = base |
| base = null |
| } |
| |
| if (base) { |
| return instantiate(base).create(options, handler) |
| } |
| |
| if (options.spdy && options.spdy.plain) { return exports.PlainServer.create(options, handler) } else { |
| return exports.Server.create(options, handler) |
| } |
| } |