testing/xpcshell/node-http2/lib/http.js

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

     1 // Public API
     2 // ==========
     4 // The main governing power behind the http2 API design is that it should look very similar to the
     5 // existing node.js [HTTPS API][1] (which is, in turn, almost identical to the [HTTP API][2]). The
     6 // additional features of HTTP/2 are exposed as extensions to this API. Furthermore, node-http2
     7 // should fall back to using HTTP/1.1 if needed. Compatibility with undocumented or deprecated
     8 // elements of the node.js HTTP/HTTPS API is a non-goal.
     9 //
    10 // Additional and modified API elements
    11 // ------------------------------------
    12 //
    13 // - **Class: http2.Endpoint**: an API for using the raw HTTP/2 framing layer. For documentation
    14 //   see the [lib/endpoint.js](endpoint.html) file.
    15 //
    16 // - **Class: http2.Server**
    17 //   - **Event: 'connection' (socket, [endpoint])**: there's a second argument if the negotiation of
    18 //     HTTP/2 was successful: the reference to the [Endpoint](endpoint.html) object tied to the
    19 //     socket.
    20 //
    21 // - **http2.createServer(options, [requestListener])**: additional option:
    22 //   - **log**: an optional [bunyan](https://github.com/trentm/node-bunyan) logger object
    23 //   - **plain**: if `true`, the server will accept HTTP/2 connections over plain TCP instead of
    24 //     TLS
    25 //
    26 // - **Class: http2.ServerResponse**
    27 //   - **response.push(options)**: initiates a server push. `options` describes the 'imaginary'
    28 //     request to which the push stream is a response; the possible options are identical to the
    29 //     ones accepted by `http2.request`. Returns a ServerResponse object that can be used to send
    30 //     the response headers and content.
    31 //
    32 // - **Class: http2.Agent**
    33 //   - **new Agent(options)**: additional option:
    34 //     - **log**: an optional [bunyan](https://github.com/trentm/node-bunyan) logger object
    35 //   - **agent.sockets**: only contains TCP sockets that corresponds to HTTP/1 requests.
    36 //   - **agent.endpoints**: contains [Endpoint](endpoint.html) objects for HTTP/2 connections.
    37 //
    38 // - **http2.request(options, [callback])**: additional option:
    39 //   - **plain**: if `true`, the client will not try to build a TLS tunnel, instead it will use
    40 //     the raw TCP stream for HTTP/2
    41 //
    42 // - **Class: http2.ClientRequest**
    43 //   - **Event: 'socket' (socket)**: in case of an HTTP/2 incoming message, `socket` is a reference
    44 //     to the associated [HTTP/2 Stream](stream.html) object (and not to the TCP socket).
    45 //   - **Event: 'push' (promise)**: signals the intention of a server push associated to this
    46 //     request. `promise` is an IncomingPromise. If there's no listener for this event, the server
    47 //     push is cancelled.
    48 //   - **request.setPriority(priority)**: assign a priority to this request. `priority` is a number
    49 //     between 0 (highest priority) and 2^31-1 (lowest priority). Default value is 2^30.
    50 //
    51 // - **Class: http2.IncomingMessage**
    52 //   - has two subclasses for easier interface description: **IncomingRequest** and
    53 //     **IncomingResponse**
    54 //   - **message.socket**: in case of an HTTP/2 incoming message, it's a reference to the associated
    55 //     [HTTP/2 Stream](stream.html) object (and not to the TCP socket).
    56 //
    57 // - **Class: http2.IncomingRequest (IncomingMessage)**
    58 //   - **message.url**: in case of an HTTP/2 incoming request, the `url` field always contains the
    59 //     path, and never a full url (it contains the path in most cases in the HTTPS api as well).
    60 //   - **message.scheme**: additional field. Mandatory HTTP/2 request metadata.
    61 //   - **message.host**: additional field. Mandatory HTTP/2 request metadata. Note that this
    62 //     replaces the old Host header field, but node-http2 will add Host to the `message.headers` for
    63 //     backwards compatibility.
    64 //
    65 // - **Class: http2.IncomingPromise (IncomingRequest)**
    66 //   - contains the metadata of the 'imaginary' request to which the server push is an answer.
    67 //   - **Event: 'response' (response)**: signals the arrival of the actual push stream. `response`
    68 //     is an IncomingResponse.
    69 //   - **Event: 'push' (promise)**: signals the intention of a server push associated to this
    70 //     request. `promise` is an IncomingPromise. If there's no listener for this event, the server
    71 //     push is cancelled.
    72 //   - **promise.cancel()**: cancels the promised server push.
    73 //   - **promise.setPriority(priority)**: assign a priority to this push stream. `priority` is a
    74 //     number between 0 (highest priority) and 2^31-1 (lowest priority). Default value is 2^30.
    75 //
    76 // API elements not yet implemented
    77 // --------------------------------
    78 //
    79 // - **Class: http2.Server**
    80 //   - **server.maxHeadersCount**
    81 //
    82 // API elements that are not applicable to HTTP/2
    83 // ----------------------------------------------
    84 //
    85 // The reason may be deprecation of certain HTTP/1.1 features, or that some API elements simply
    86 // don't make sense when using HTTP/2. These will not be present when a request is done with HTTP/2,
    87 // but will function normally when falling back to using HTTP/1.1.
    88 //
    89 // - **Class: http2.Server**
    90 //   - **Event: 'checkContinue'**: not in the spec, yet (see [http-spec#18][expect-continue])
    91 //   - **Event: 'upgrade'**: upgrade is deprecated in HTTP/2
    92 //   - **Event: 'timeout'**: HTTP/2 sockets won't timeout because of application level keepalive
    93 //     (PING frames)
    94 //   - **Event: 'connect'**: not in the spec, yet (see [http-spec#230][connect])
    95 //   - **server.setTimeout(msecs, [callback])**
    96 //   - **server.timeout**
    97 //
    98 // - **Class: http2.ServerResponse**
    99 //   - **Event: 'close'**
   100 //   - **Event: 'timeout'**
   101 //   - **response.writeContinue()**
   102 //   - **response.writeHead(statusCode, [reasonPhrase], [headers])**: reasonPhrase will always be
   103 //     ignored since [it's not supported in HTTP/2][3]
   104 //   - **response.setTimeout(timeout, [callback])**
   105 //
   106 // - **Class: http2.Agent**
   107 //   - **agent.maxSockets**: only affects HTTP/1 connection pool. When using HTTP/2, there's always
   108 //     one connection per host.
   109 //
   110 // - **Class: http2.ClientRequest**
   111 //   - **Event: 'upgrade'**
   112 //   - **Event: 'connect'**
   113 //   - **Event: 'continue'**
   114 //   - **request.setTimeout(timeout, [callback])**
   115 //   - **request.setNoDelay([noDelay])**
   116 //   - **request.setSocketKeepAlive([enable], [initialDelay])**
   117 //
   118 // - **Class: http2.IncomingMessage**
   119 //   - **Event: 'close'**
   120 //   - **message.setTimeout(timeout, [callback])**
   121 //
   122 // [1]: http://nodejs.org/api/https.html
   123 // [2]: http://nodejs.org/api/http.html
   124 // [3]: http://tools.ietf.org/html/draft-ietf-httpbis-http2-10#section-8.1.3.2
   125 // [expect-continue]: https://github.com/http2/http2-spec/issues/18
   126 // [connect]: https://github.com/http2/http2-spec/issues/230
   128 // Common server and client side code
   129 // ==================================
   131 var net = require('net');
   132 var url = require('url');
   133 var util = require('util');
   134 var EventEmitter = require('events').EventEmitter;
   135 var PassThrough = require('stream').PassThrough;
   136 var Readable = require('stream').Readable;
   137 var Writable = require('stream').Writable;
   138 var Endpoint = require('http2-protocol').Endpoint;
   139 var implementedVersion = require('http2-protocol').ImplementedVersion;
   140 var http = require('http');
   141 var https = require('https');
   143 exports.STATUS_CODES = http.STATUS_CODES;
   144 exports.IncomingMessage = IncomingMessage;
   145 exports.OutgoingMessage = OutgoingMessage;
   147 var deprecatedHeaders = [
   148   'connection',
   149   'host',
   150   'keep-alive',
   151   'proxy-connection',
   152   'te',
   153   'transfer-encoding',
   154   'upgrade'
   155 ];
   157 // When doing NPN/ALPN negotiation, HTTP/1.1 is used as fallback
   158 var supportedProtocols = [implementedVersion, 'http/1.1', 'http/1.0'];
   160 // Logging
   161 // -------
   163 // Logger shim, used when no logger is provided by the user.
   164 function noop() {}
   165 var defaultLogger = {
   166   fatal: noop,
   167   error: noop,
   168   warn : noop,
   169   info : noop,
   170   debug: noop,
   171   trace: noop,
   173   child: function() { return this; }
   174 };
   176 // Bunyan serializers exported by submodules that are worth adding when creating a logger.
   177 exports.serializers = require('http2-protocol').serializers;
   179 // IncomingMessage class
   180 // ---------------------
   182 function IncomingMessage(stream) {
   183   // * This is basically a read-only wrapper for the [Stream](stream.html) class.
   184   PassThrough.call(this);
   185   stream.pipe(this);
   186   this.socket = this.stream = stream;
   188   this._log = stream._log.child({ component: 'http' });
   190   // * HTTP/2.0 does not define a way to carry the version identifier that is included in the
   191   //   HTTP/1.1 request/status line. Version is always 2.0.
   192   this.httpVersion = '2.0';
   193   this.httpVersionMajor = 2;
   194   this.httpVersionMinor = 0;
   196   // * `this.headers` will store the regular headers (and none of the special colon headers)
   197   this.headers = {};
   198   this.trailers = undefined;
   199   this._lastHeadersSeen = undefined;
   201   // * Other metadata is filled in when the headers arrive.
   202   stream.once('headers', this._onHeaders.bind(this));
   203   stream.once('end', this._onEnd.bind(this));
   204 }
   205 IncomingMessage.prototype = Object.create(PassThrough.prototype, { constructor: { value: IncomingMessage } });
   207 // [Request Header Fields](http://tools.ietf.org/html/draft-ietf-httpbis-http2-10#section-8.1.3.1)
   208 // * `headers` argument: HTTP/2.0 request and response header fields carry information as a series
   209 //   of key-value pairs. This includes the target URI for the request, the status code for the
   210 //   response, as well as HTTP header fields.
   211 IncomingMessage.prototype._onHeaders = function _onHeaders(headers) {
   212   // * An HTTP/2.0 request or response MUST NOT include any of the following header fields:
   213   //   Connection, Host, Keep-Alive, Proxy-Connection, TE, Transfer-Encoding, and Upgrade. A server
   214   //   MUST treat the presence of any of these header fields as a stream error of type
   215   //   PROTOCOL_ERROR.
   216   for (var i = 0; i < deprecatedHeaders.length; i++) {
   217     var key = deprecatedHeaders[i];
   218     if (key in headers) {
   219       this._log.error({ key: key, value: headers[key] }, 'Deprecated header found');
   220       this.stream.emit('error', 'PROTOCOL_ERROR');
   221       return;
   222     }
   223   }
   225   // * Store the _regular_ headers in `this.headers`
   226   for (var name in headers) {
   227     if (name[0] !== ':') {
   228       this.headers[name] = headers[name];
   229     }
   230   }
   232   // * The last header block, if it's not the first, will represent the trailers
   233   var self = this;
   234   this.stream.on('headers', function(headers) {
   235     self._lastHeadersSeen = headers;
   236   });
   237 };
   239 IncomingMessage.prototype._onEnd = function _onEnd() {
   240   this.trailers = this._lastHeadersSeen;
   241 };
   243 IncomingMessage.prototype.setTimeout = noop;
   245 IncomingMessage.prototype._checkSpecialHeader = function _checkSpecialHeader(key, value) {
   246   if ((typeof value !== 'string') || (value.length === 0)) {
   247     this._log.error({ key: key, value: value }, 'Invalid or missing special header field');
   248     this.stream.emit('error', 'PROTOCOL_ERROR');
   249   }
   251   return value;
   252 }
   253 ;
   255 // OutgoingMessage class
   256 // ---------------------
   258 function OutgoingMessage() {
   259   // * This is basically a read-only wrapper for the [Stream](stream.html) class.
   260   Writable.call(this);
   262   this._headers = {};
   263   this._trailers = undefined;
   264   this.headersSent = false;
   266   this.on('finish', this._finish);
   267 }
   268 OutgoingMessage.prototype = Object.create(Writable.prototype, { constructor: { value: OutgoingMessage } });
   270 OutgoingMessage.prototype._write = function _write(chunk, encoding, callback) {
   271   if (this.stream) {
   272     this.stream.write(chunk, encoding, callback);
   273   } else {
   274     this.once('socket', this._write.bind(this, chunk, encoding, callback));
   275   }
   276 };
   278 OutgoingMessage.prototype._finish = function _finish() {
   279   if (this.stream) {
   280     if (this._trailers) {
   281       if (this.request) {
   282         this.request.addTrailers(this._trailers);
   283       } else {
   284         this.stream.headers(this._trailers);
   285       }
   286     }
   287     this.stream.end();
   288   } else {
   289     this.once('socket', this._finish.bind(this));
   290   }
   291 };
   293 OutgoingMessage.prototype.setHeader = function setHeader(name, value) {
   294   if (this.headersSent) {
   295     throw new Error('Can\'t set headers after they are sent.');
   296   } else {
   297     name = name.toLowerCase();
   298     if (deprecatedHeaders.indexOf(name) !== -1) {
   299       throw new Error('Cannot set deprecated header: ' + name);
   300     }
   301     this._headers[name] = value;
   302   }
   303 };
   305 OutgoingMessage.prototype.removeHeader = function removeHeader(name) {
   306   if (this.headersSent) {
   307     throw new Error('Can\'t remove headers after they are sent.');
   308   } else {
   309     delete this._headers[name.toLowerCase()];
   310   }
   311 };
   313 OutgoingMessage.prototype.getHeader = function getHeader(name) {
   314   return this._headers[name.toLowerCase()];
   315 };
   317 OutgoingMessage.prototype.addTrailers = function addTrailers(trailers) {
   318   this._trailers = trailers;
   319 };
   321 OutgoingMessage.prototype.setTimeout = noop;
   323 OutgoingMessage.prototype._checkSpecialHeader = IncomingMessage.prototype._checkSpecialHeader;
   325 // Server side
   326 // ===========
   328 exports.createServer = createServer;
   329 exports.Server = Server;
   330 exports.IncomingRequest = IncomingRequest;
   331 exports.OutgoingResponse = OutgoingResponse;
   332 exports.ServerResponse = OutgoingResponse; // for API compatibility
   334 // Server class
   335 // ------------
   337 function Server(options) {
   338   options = util._extend({}, options);
   340   this._log = (options.log || defaultLogger).child({ component: 'http' });
   341   this._settings = options.settings;
   343   var start = this._start.bind(this);
   344   var fallback = this._fallback.bind(this);
   346   // HTTP2 over TLS (using NPN or ALPN)
   347   if ((options.key && options.cert) || options.pfx) {
   348     this._log.info('Creating HTTP/2 server over TLS');
   349     this._mode = 'tls';
   350     options.ALPNProtocols = supportedProtocols;
   351     options.NPNProtocols = supportedProtocols;
   352     this._server = https.createServer(options);
   353     this._originalSocketListeners = this._server.listeners('secureConnection');
   354     this._server.removeAllListeners('secureConnection');
   355     this._server.on('secureConnection', function(socket) {
   356       var negotiatedProtocol = socket.alpnProtocol || socket.npnProtocol;
   357       if ((negotiatedProtocol === implementedVersion) && socket.servername) {
   358         start(socket);
   359       } else {
   360         fallback(socket);
   361       }
   362     });
   363     this._server.on('request', this.emit.bind(this, 'request'));
   364   }
   366   // HTTP2 over plain TCP
   367   else if (options.plain) {
   368     this._log.info('Creating HTTP/2 server over plain TCP');
   369     this._mode = 'plain';
   370     this._server = net.createServer(start);
   371   }
   373   // HTTP/2 with HTTP/1.1 upgrade
   374   else {
   375     this._log.error('Trying to create HTTP/2 server with Upgrade from HTTP/1.1');
   376     throw new Error('HTTP1.1 -> HTTP2 upgrade is not yet supported. Please provide TLS keys.');
   377   }
   379   this._server.on('close', this.emit.bind(this, 'close'));
   380 }
   381 Server.prototype = Object.create(EventEmitter.prototype, { constructor: { value: Server } });
   383 // Starting HTTP/2
   384 Server.prototype._start = function _start(socket) {
   385   var endpoint = new Endpoint(this._log, 'SERVER', this._settings);
   387   this._log.info({ e: endpoint,
   388                    client: socket.remoteAddress + ':' + socket.remotePort,
   389                    SNI: socket.servername
   390                  }, 'New incoming HTTP/2 connection');
   392   endpoint.pipe(socket).pipe(endpoint);
   394   var self = this;
   395   endpoint.on('stream', function _onStream(stream) {
   396     var response = new OutgoingResponse(stream);
   397     var request = new IncomingRequest(stream);
   399     request.once('ready', self.emit.bind(self, 'request', request, response));
   400   });
   402   endpoint.on('error', this.emit.bind(this, 'clientError'));
   403   socket.on('error', this.emit.bind(this, 'clientError'));
   405   this.emit('connection', socket, endpoint);
   406 };
   408 Server.prototype._fallback = function _fallback(socket) {
   409   var negotiatedProtocol = socket.alpnProtocol || socket.npnProtocol;
   411   this._log.info({ client: socket.remoteAddress + ':' + socket.remotePort,
   412                    protocol: negotiatedProtocol,
   413                    SNI: socket.servername
   414                  }, 'Falling back to simple HTTPS');
   416   for (var i = 0; i < this._originalSocketListeners.length; i++) {
   417     this._originalSocketListeners[i].call(this._server, socket);
   418   }
   420   this.emit('connection', socket);
   421 };
   423 // There are [3 possible signatures][1] of the `listen` function. Every arguments is forwarded to
   424 // the backing TCP or HTTPS server.
   425 // [1]: http://nodejs.org/api/http.html#http_server_listen_port_hostname_backlog_callback
   426 Server.prototype.listen = function listen(port, hostname) {
   427   this._log.info({ on: ((typeof hostname === 'string') ? (hostname + ':' + port) : port) },
   428                  'Listening for incoming connections');
   429   this._server.listen.apply(this._server, arguments);
   430 };
   432 Server.prototype.close = function close(callback) {
   433   this._log.info('Closing server');
   434   this._server.close(callback);
   435 };
   437 Server.prototype.setTimeout = function setTimeout(timeout, callback) {
   438   if (this._mode === 'tls') {
   439     this._server.setTimeout(timeout, callback);
   440   }
   441 };
   443 Object.defineProperty(Server.prototype, 'timeout', {
   444   get: function getTimeout() {
   445     if (this._mode === 'tls') {
   446       return this._server.timeout;
   447     } else {
   448       return undefined;
   449     }
   450   },
   451   set: function setTimeout(timeout) {
   452     if (this._mode === 'tls') {
   453       this._server.timeout = timeout;
   454     }
   455   }
   456 });
   458 // Overriding `EventEmitter`'s `on(event, listener)` method to forward certain subscriptions to
   459 // `server`.There are events on the `http.Server` class where it makes difference whether someone is
   460 // listening on the event or not. In these cases, we can not simply forward the events from the
   461 // `server` to `this` since that means a listener. Instead, we forward the subscriptions.
   462 Server.prototype.on = function on(event, listener) {
   463   if ((event === 'upgrade') || (event === 'timeout')) {
   464     this._server.on(event, listener && listener.bind(this));
   465   } else {
   466     EventEmitter.prototype.on.call(this, event, listener);
   467   }
   468 };
   470 // `addContext` is used to add Server Name Indication contexts
   471 Server.prototype.addContext = function addContext(hostname, credentials) {
   472   if (this._mode === 'tls') {
   473     this._server.addContext(hostname, credentials);
   474   }
   475 };
   477 function createServer(options, requestListener) {
   478   if (typeof options === 'function') {
   479     requestListener = options;
   480     options = undefined;
   481   }
   483   var server = new Server(options);
   485   if (requestListener) {
   486     server.on('request', requestListener);
   487   }
   489   return server;
   490 }
   492 // IncomingRequest class
   493 // ---------------------
   495 function IncomingRequest(stream) {
   496   IncomingMessage.call(this, stream);
   497 }
   498 IncomingRequest.prototype = Object.create(IncomingMessage.prototype, { constructor: { value: IncomingRequest } });
   500 // [Request Header Fields](http://tools.ietf.org/html/draft-ietf-httpbis-http2-10#section-8.1.3.1)
   501 // * `headers` argument: HTTP/2.0 request and response header fields carry information as a series
   502 //   of key-value pairs. This includes the target URI for the request, the status code for the
   503 //   response, as well as HTTP header fields.
   504 IncomingRequest.prototype._onHeaders = function _onHeaders(headers) {
   505   // * The ":method" header field includes the HTTP method
   506   // * The ":scheme" header field includes the scheme portion of the target URI
   507   // * The ":authority" header field includes the authority portion of the target URI
   508   // * The ":path" header field includes the path and query parts of the target URI.
   509   //   This field MUST NOT be empty; URIs that do not contain a path component MUST include a value
   510   //   of '/', unless the request is an OPTIONS request for '*', in which case the ":path" header
   511   //   field MUST include '*'.
   512   // * All HTTP/2.0 requests MUST include exactly one valid value for all of these header fields. A
   513   //   server MUST treat the absence of any of these header fields, presence of multiple values, or
   514   //   an invalid value as a stream error of type PROTOCOL_ERROR.
   515   this.method = this._checkSpecialHeader(':method'   , headers[':method']);
   516   this.scheme = this._checkSpecialHeader(':scheme'   , headers[':scheme']);
   517   this.host   = this._checkSpecialHeader(':authority', headers[':authority']  );
   518   this.url    = this._checkSpecialHeader(':path'     , headers[':path']  );
   520   // * Host header is included in the headers object for backwards compatibility.
   521   this.headers.host = this.host;
   523   // * Handling regular headers.
   524   IncomingMessage.prototype._onHeaders.call(this, headers);
   526   // * Signaling that the headers arrived.
   527   this._log.info({ method: this.method, scheme: this.scheme, host: this.host,
   528                    path: this.url, headers: this.headers }, 'Incoming request');
   529   this.emit('ready');
   530 };
   532 // OutgoingResponse class
   533 // ----------------------
   535 function OutgoingResponse(stream) {
   536   OutgoingMessage.call(this);
   538   this._log = stream._log.child({ component: 'http' });
   540   this.stream = stream;
   541   this.statusCode = 200;
   542   this.sendDate = true;
   544   this.stream.once('headers', this._onRequestHeaders.bind(this));
   545 }
   546 OutgoingResponse.prototype = Object.create(OutgoingMessage.prototype, { constructor: { value: OutgoingResponse } });
   548 OutgoingResponse.prototype.writeHead = function writeHead(statusCode, reasonPhrase, headers) {
   549   if (typeof reasonPhrase === 'string') {
   550     this._log.warn('Reason phrase argument was present but ignored by the writeHead method');
   551   } else {
   552     headers = reasonPhrase;
   553   }
   555   for (var name in headers) {
   556     this.setHeader(name, headers[name]);
   557   }
   558   headers = this._headers;
   560   if (this.sendDate && !('date' in this._headers)) {
   561     headers.date = (new Date()).toUTCString();
   562   }
   564   this._log.info({ status: statusCode, headers: this._headers }, 'Sending server response');
   566   headers[':status'] = this.statusCode = statusCode;
   568   this.stream.headers(headers);
   569   this.headersSent = true;
   570 };
   572 OutgoingResponse.prototype._implicitHeaders = function _implicitHeaders() {
   573   if (!this.headersSent) {
   574     this.writeHead(this.statusCode);
   575   }
   576 };
   578 OutgoingResponse.prototype.write = function write() {
   579   this._implicitHeaders();
   580   return OutgoingMessage.prototype.write.apply(this, arguments);
   581 };
   583 OutgoingResponse.prototype.end = function end() {
   584   this._implicitHeaders();
   585   return OutgoingMessage.prototype.end.apply(this, arguments);
   586 };
   588 OutgoingResponse.prototype._onRequestHeaders = function _onRequestHeaders(headers) {
   589   this._requestHeaders = headers;
   590 };
   592 OutgoingResponse.prototype.push = function push(options) {
   593   if (typeof options === 'string') {
   594     options = url.parse(options);
   595   }
   597   if (!options.path) {
   598     throw new Error('`path` option is mandatory.');
   599   }
   601   var promise = util._extend({
   602     ':method': (options.method || 'GET').toUpperCase(),
   603     ':scheme': (options.protocol && options.protocol.slice(0, -1)) || this._requestHeaders[':scheme'],
   604     ':authority': options.hostname || options.host || this._requestHeaders[':authority'],
   605     ':path': options.path
   606   }, options.headers);
   608   this._log.info({ method: promise[':method'], scheme: promise[':scheme'],
   609                    authority: promise[':authority'], path: promise[':path'],
   610                    headers: options.headers }, 'Promising push stream');
   612   var pushStream = this.stream.promise(promise);
   614   return new OutgoingResponse(pushStream);
   615 };
   617 // Overriding `EventEmitter`'s `on(event, listener)` method to forward certain subscriptions to
   618 // `request`. See `Server.prototype.on` for explanation.
   619 OutgoingResponse.prototype.on = function on(event, listener) {
   620   if (this.request && (event === 'timeout')) {
   621     this.request.on(event, listener && listener.bind(this));
   622   } else {
   623     OutgoingMessage.prototype.on.call(this, event, listener);
   624   }
   625 };
   627 // Client side
   628 // ===========
   630 exports.ClientRequest = OutgoingRequest; // for API compatibility
   631 exports.OutgoingRequest = OutgoingRequest;
   632 exports.IncomingResponse = IncomingResponse;
   633 exports.Agent = Agent;
   634 exports.globalAgent = undefined;
   635 exports.request = function request(options, callback) {
   636   return (options.agent || exports.globalAgent).request(options, callback);
   637 };
   638 exports.get = function get(options, callback) {
   639   return (options.agent || exports.globalAgent).get(options, callback);
   640 };
   642 // Agent class
   643 // -----------
   645 function Agent(options) {
   646   EventEmitter.call(this);
   648   options = util._extend({}, options);
   650   this._settings = options.settings;
   651   this._log = (options.log || defaultLogger).child({ component: 'http' });
   652   this.endpoints = {};
   654   // * Using an own HTTPS agent, because the global agent does not look at `NPN/ALPNProtocols` when
   655   //   generating the key identifying the connection, so we may get useless non-negotiated TLS
   656   //   channels even if we ask for a negotiated one. This agent will contain only negotiated
   657   //   channels.
   658   var agentOptions = {};
   659   agentOptions.ALPNProtocols = supportedProtocols;
   660   agentOptions.NPNProtocols = supportedProtocols;
   661   this._httpsAgent = new https.Agent(agentOptions);
   663   this.sockets = this._httpsAgent.sockets;
   664   this.requests = this._httpsAgent.requests;
   665 }
   666 Agent.prototype = Object.create(EventEmitter.prototype, { constructor: { value: Agent } });
   668 Agent.prototype.request = function request(options, callback) {
   669   if (typeof options === 'string') {
   670     options = url.parse(options);
   671   } else {
   672     options = util._extend({}, options);
   673   }
   675   options.method = (options.method || 'GET').toUpperCase();
   676   options.protocol = options.protocol || 'https:';
   677   options.host = options.hostname || options.host || 'localhost';
   678   options.port = options.port || 443;
   679   options.path = options.path || '/';
   681   if (!options.plain && options.protocol === 'http:') {
   682     this._log.error('Trying to negotiate client request with Upgrade from HTTP/1.1');
   683     throw new Error('HTTP1.1 -> HTTP2 upgrade is not yet supported.');
   684   }
   686   var request = new OutgoingRequest(this._log);
   688   if (callback) {
   689     request.on('response', callback);
   690   }
   692   var key = [
   693     !!options.plain,
   694     options.host,
   695     options.port
   696   ].join(':');
   698   // * There's an existing HTTP/2 connection to this host
   699   if (key in this.endpoints) {
   700     var endpoint = this.endpoints[key];
   701     request._start(endpoint.createStream(), options);
   702   }
   704   // * HTTP/2 over plain TCP
   705   else if (options.plain) {
   706     endpoint = new Endpoint(this._log, 'CLIENT', this._settings);
   707     endpoint.socket = net.connect({
   708       host: options.host,
   709       port: options.port,
   710       localAddress: options.localAddress
   711     });
   712     endpoint.pipe(endpoint.socket).pipe(endpoint);
   713     request._start(endpoint.createStream(), options);
   714   }
   716   // * HTTP/2 over TLS negotiated using NPN or ALPN
   717   else {
   718     var started = false;
   719     options.ALPNProtocols = supportedProtocols;
   720     options.NPNProtocols = supportedProtocols;
   721     options.servername = options.host; // Server Name Indication
   722     options.agent = this._httpsAgent;
   723     var httpsRequest = https.request(options);
   725     httpsRequest.on('socket', function(socket) {
   726       var negotiatedProtocol = socket.alpnProtocol || socket.npnProtocol;
   727       if (negotiatedProtocol !== undefined) {
   728         negotiated();
   729       } else {
   730         socket.on('secureConnect', negotiated);
   731       }
   732     });
   734     var self = this;
   735     function negotiated() {
   736       var endpoint;
   737       var negotiatedProtocol = httpsRequest.socket.alpnProtocol || httpsRequest.socket.npnProtocol;
   738       if (negotiatedProtocol === implementedVersion) {
   739         httpsRequest.socket.emit('agentRemove');
   740         unbundleSocket(httpsRequest.socket);
   741         endpoint = new Endpoint(self._log, 'CLIENT', self._settings);
   742         endpoint.socket = httpsRequest.socket;
   743         endpoint.pipe(endpoint.socket).pipe(endpoint);
   744       }
   745       if (started) {
   746         if (endpoint) {
   747           endpoint.close();
   748         } else {
   749           httpsRequest.abort();
   750         }
   751       } else {
   752         if (endpoint) {
   753           self._log.info({ e: endpoint, server: options.host + ':' + options.port },
   754                          'New outgoing HTTP/2 connection');
   755           self.endpoints[key] = endpoint;
   756           self.emit(key, endpoint);
   757         } else {
   758           self.emit(key, undefined);
   759         }
   760       }
   761     }
   763     this.once(key, function(endpoint) {
   764       started = true;
   765       if (endpoint) {
   766         request._start(endpoint.createStream(), options);
   767       } else {
   768         request._fallback(httpsRequest);
   769       }
   770     });
   771   }
   773   return request;
   774 };
   776 Agent.prototype.get = function get(options, callback) {
   777   var request = this.request(options, callback);
   778   request.end();
   779   return request;
   780 };
   782 function unbundleSocket(socket) {
   783   socket.removeAllListeners('data');
   784   socket.removeAllListeners('end');
   785   socket.removeAllListeners('readable');
   786   socket.removeAllListeners('close');
   787   socket.removeAllListeners('error');
   788   socket.unpipe();
   789   delete socket.ondata;
   790   delete socket.onend;
   791 }
   793 Object.defineProperty(Agent.prototype, 'maxSockets', {
   794   get: function getMaxSockets() {
   795     return this._httpsAgent.maxSockets;
   796   },
   797   set: function setMaxSockets(value) {
   798     this._httpsAgent.maxSockets = value;
   799   }
   800 });
   802 exports.globalAgent = new Agent();
   804 // OutgoingRequest class
   805 // ---------------------
   807 function OutgoingRequest() {
   808   OutgoingMessage.call(this);
   810   this._log = undefined;
   812   this.stream = undefined;
   813 }
   814 OutgoingRequest.prototype = Object.create(OutgoingMessage.prototype, { constructor: { value: OutgoingRequest } });
   816 OutgoingRequest.prototype._start = function _start(stream, options) {
   817   this.stream = stream;
   819   this._log = stream._log.child({ component: 'http' });
   821   for (var key in options.headers) {
   822     this.setHeader(key, options.headers[key]);
   823   }
   824   var headers = this._headers;
   825   delete headers.host;
   827   if (options.auth) {
   828     headers.authorization = 'Basic ' + new Buffer(options.auth).toString('base64');
   829   }
   831   headers[':scheme'] = options.protocol.slice(0, -1);
   832   headers[':method'] = options.method;
   833   headers[':authority'] = options.host;
   834   headers[':path'] = options.path;
   836   this._log.info({ scheme: headers[':scheme'], method: headers[':method'],
   837                    authority: headers[':authority'], path: headers[':path'],
   838                    headers: (options.headers || {}) }, 'Sending request');
   839   this.stream.headers(headers);
   840   this.headersSent = true;
   842   this.emit('socket', this.stream);
   844   var response = new IncomingResponse(this.stream);
   845   response.once('ready', this.emit.bind(this, 'response', response));
   847   this.stream.on('promise', this._onPromise.bind(this));
   848 };
   850 OutgoingRequest.prototype._fallback = function _fallback(request) {
   851   request.on('response', this.emit.bind(this, 'response'));
   852   this.stream = this.request = request;
   853   this.emit('socket', this.socket);
   854 };
   856 OutgoingRequest.prototype.setPriority = function setPriority(priority) {
   857   if (this.stream) {
   858     this.stream.priority(priority);
   859   } else {
   860     this.once('socket', this.setPriority.bind(this, priority));
   861   }
   862 };
   864 // Overriding `EventEmitter`'s `on(event, listener)` method to forward certain subscriptions to
   865 // `request`. See `Server.prototype.on` for explanation.
   866 OutgoingRequest.prototype.on = function on(event, listener) {
   867   if (this.request && (event === 'upgrade')) {
   868     this.request.on(event, listener && listener.bind(this));
   869   } else {
   870     OutgoingMessage.prototype.on.call(this, event, listener);
   871   }
   872 };
   874 // Methods only in fallback mode
   875 OutgoingRequest.prototype.setNoDelay = function setNoDelay(noDelay) {
   876   if (this.request) {
   877     this.request.setNoDelay(noDelay);
   878   } else if (!this.stream) {
   879     this.on('socket', this.setNoDelay.bind(this, noDelay));
   880   }
   881 };
   883 OutgoingRequest.prototype.setSocketKeepAlive = function setSocketKeepAlive(enable, initialDelay) {
   884   if (this.request) {
   885     this.request.setSocketKeepAlive(enable, initialDelay);
   886   } else if (!this.stream) {
   887     this.on('socket', this.setSocketKeepAlive.bind(this, enable, initialDelay));
   888   }
   889 };
   891 OutgoingRequest.prototype.setTimeout = function setTimeout(timeout, callback) {
   892   if (this.request) {
   893     this.request.setTimeout(timeout, callback);
   894   } else if (!this.stream) {
   895     this.on('socket', this.setTimeout.bind(this, timeout, callback));
   896   }
   897 };
   899 // Aborting the request
   900 OutgoingRequest.prototype.abort = function abort() {
   901   if (this.request) {
   902     this.request.abort();
   903   } else if (this.stream) {
   904     this.stream.reset('CANCEL');
   905   } else {
   906     this.on('socket', this.abort.bind(this));
   907   }
   908 };
   910 // Receiving push promises
   911 OutgoingRequest.prototype._onPromise = function _onPromise(stream, headers) {
   912   this._log.info({ push_stream: stream.id }, 'Receiving push promise');
   914   var promise = new IncomingPromise(stream, headers);
   916   if (this.listeners('push').length > 0) {
   917     this.emit('push', promise);
   918   } else {
   919     promise.cancel();
   920   }
   921 };
   923 // IncomingResponse class
   924 // ----------------------
   926 function IncomingResponse(stream) {
   927   IncomingMessage.call(this, stream);
   928 }
   929 IncomingResponse.prototype = Object.create(IncomingMessage.prototype, { constructor: { value: IncomingResponse } });
   931 // [Response Header Fields](http://tools.ietf.org/html/draft-ietf-httpbis-http2-10#section-8.1.3.2)
   932 // * `headers` argument: HTTP/2.0 request and response header fields carry information as a series
   933 //   of key-value pairs. This includes the target URI for the request, the status code for the
   934 //   response, as well as HTTP header fields.
   935 IncomingResponse.prototype._onHeaders = function _onHeaders(headers) {
   936   // * A single ":status" header field is defined that carries the HTTP status code field. This
   937   //   header field MUST be included in all responses.
   938   // * A client MUST treat the absence of the ":status" header field, the presence of multiple
   939   //   values, or an invalid value as a stream error of type PROTOCOL_ERROR.
   940   //   Note: currently, we do not enforce it strictly: we accept any format, and parse it as int
   941   // * HTTP/2.0 does not define a way to carry the reason phrase that is included in an HTTP/1.1
   942   //   status line.
   943   this.statusCode = parseInt(this._checkSpecialHeader(':status', headers[':status']));
   945   // * Handling regular headers.
   946   IncomingMessage.prototype._onHeaders.call(this, headers);
   948   // * Signaling that the headers arrived.
   949   this._log.info({ status: this.statusCode, headers: this.headers}, 'Incoming response');
   950   this.emit('ready');
   951 };
   953 // IncomingPromise class
   954 // -------------------------
   956 function IncomingPromise(responseStream, promiseHeaders) {
   957   var stream = new Readable();
   958   stream._read = noop;
   959   stream.push(null);
   960   stream._log = responseStream._log;
   962   IncomingRequest.call(this, stream);
   964   this._onHeaders(promiseHeaders);
   966   this._responseStream = responseStream;
   968   var response = new IncomingResponse(this._responseStream);
   969   response.once('ready', this.emit.bind(this, 'response', response));
   971   this.stream.on('promise', this._onPromise.bind(this));
   972 }
   973 IncomingPromise.prototype = Object.create(IncomingRequest.prototype, { constructor: { value: IncomingPromise } });
   975 IncomingPromise.prototype.cancel = function cancel() {
   976   this._responseStream.reset('CANCEL');
   977 };
   979 IncomingPromise.prototype.setPriority = function setPriority(priority) {
   980   this._responseStream.priority(priority);
   981 };
   983 IncomingPromise.prototype._onPromise = OutgoingRequest.prototype._onPromise;

mercurial