From 234def4b498dd4f3e07fad9d7faac198626536d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Moln=C3=A1r?= Date: Sun, 18 Aug 2013 00:50:08 +0200 Subject: [PATCH] HTTP: initial Agent implementation --- lib/http.js | 144 +++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 102 insertions(+), 42 deletions(-) diff --git a/lib/http.js b/lib/http.js index 47ad5e1..7eb3586 100644 --- a/lib/http.js +++ b/lib/http.js @@ -257,63 +257,123 @@ ServerResponse.prototype.writeHead = function writeHead(statusCode, reasonPhrase // ====== http2.request = function request(options, callback) { + return globalAgent.request(options, callback); +}; + +http2.get = function get(options, callback) { +}; + +// Agent class +// ----------- + +function Agent(options) { + EventEmitter.call(this); + + this._options = options || {}; + this._log = (this._options.log || logging.root).child({ component: 'http' }); + this._endpoints = {}; + this._settings = this._options.settings || default_settings; + + // * Using an own HTTPS agent, because the global agent does not look at `NPNProtocols` when + // generating the key identifying the connection, so we may get useless non-negotiated TLS + // channels even if we ask for a negotiated one. This agent will contain only negotiated + // channels. + this._httpsAgent = new https.Agent({ + NPNProtocols: [implementedVersion, 'http/1.1', 'http/1.0'] + }); +} +Agent.prototype = Object.create(EventEmitter.prototype, { constructor: { value: Agent } }); + +Agent.prototype.request = function request(options, callback) { + if (options.protocol === 'http') { + this._log.error('Trying to negotiate client request with Upgrade from HTTP/1.1'); + throw new Error('HTTP1.1 -> HTTP2 upgrade is not yet supported.'); + } + var request = new ClientRequest(logging.root); if (callback) { request.on('response', callback); } - var tlsOptions = { - host: options.hostname || options.host, - port: options.port || 80, - NPNProtocols: [implementedVersion, 'http/1.1', 'http/1.0'] - }; - - var optionsToForward = [ - 'pfx', - 'key', - 'passphrase', - 'cert', - 'ca', - 'ciphers', - 'rejectUnauthorized', - 'secureProtocol' - ]; - for (var i = 0; i < optionsToForward.length; i++) { - var key = optionsToForward[i]; - if (key in options) { - tlsOptions[key] = options[key]; - } + if (options.protocol === 'http') { + throw new Error('Protocol:' + options.protocol + ' not supported.'); + } + if (options.port === undefined) { + options.port = 433; } - var socket = tls.connect(tlsOptions, function() { - // HTTP2 is supported! - if (socket.npnProtocol === implementedVersion) { - var endpoint = new Endpoint('CLIENT', options._settings || default_settings); - endpoint.pipe(socket).pipe(endpoint); - request._start(endpoint.createStream(), options); - } + var key = [ + options.host || 'localhost', + options.port + ].join(':'); - // Fallback - else { - socket.end(); - request._fallback(https.request(options)); - } - }); + // * There's an existing HTTP/2 connection to this host + if (key in this._endpoints) { + var endpoint = this._endpoints[key]; + request._start(endpoint.createStream(), options); + } - return request; -}; + // * HTTP/2 over TLS negotiated using NPN (or later ALPN) + // * if the negotiation is unsuccessful + // * adding socket to the HTTPS agent's socket pool + // * initiating a request with the HTTPS agent + // * calling request's fallback() to fall back to use the new request object + else { + var started = false; + options.NPNProtocols = [implementedVersion, 'http/1.1', 'http/1.0']; + options.agent = this._httpsAgent; + var httpsRequest = https.request(options); + httpsRequest.on('socket', function(socket) { + if (socket.npnProtocol !== undefined) { + negotiated(); + } else { + socket.on('secureConnect', negotiated); + } + }); + + var negotiated = function negotiated() { + if (!started) { + if (httpsRequest.socket.npnProtocol === implementedVersion) { + httpsRequest.socket.emit('agentRemove'); + unbundleSocket(httpsRequest.socket); + var logger = this._log.child({ server: options.host + ':' + options.port }); + var endpoint = new Endpoint('CLIENT', this._settings, logger); + endpoint.pipe(httpsRequest.socket).pipe(endpoint); + this._endpoints[key] = endpoint; + this.emit(key, endpoint); + } else { + this.emit(key, undefined); + } + } + }.bind(this); + + this.once(key, function(endpoint) { + started = true; + if (endpoint) { + request._start(endpoint.createStream(), options); + } else { + request._fallback(httpsRequest); + } + }); + } -http2.get = function get(options, callback) { + return request; }; -// Agent class -// ----------- - -function Agent(options) { - +function unbundleSocket(socket) { + socket.removeAllListeners('data'); + socket.removeAllListeners('end'); + socket.removeAllListeners('readable'); + socket.removeAllListeners('close'); + socket.removeAllListeners('error'); + socket.unpipe(); + delete socket.ondata; + delete socket.onend; } +var globalAgent = http2.globalAgent = new Agent(); + // ClientRequest // -------------