forked from mirrors/gecko-dev
		
	 5b201c8a70
			
		
	
	
		5b201c8a70
		
	
	
	
	
		
			
			a test for 1124717 ran into
https://github.com/molnarg/node-http2/issues/89 which is fixed in this update
		
	
			
		
			
				
	
	
		
			1139 lines
		
	
	
	
		
			39 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			1139 lines
		
	
	
	
		
			39 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| // Public API
 | |
| // ==========
 | |
| 
 | |
| // The main governing power behind the http2 API design is that it should look very similar to the
 | |
| // existing node.js [HTTPS API][1] (which is, in turn, almost identical to the [HTTP API][2]). The
 | |
| // additional features of HTTP/2 are exposed as extensions to this API. Furthermore, node-http2
 | |
| // should fall back to using HTTP/1.1 if needed. Compatibility with undocumented or deprecated
 | |
| // elements of the node.js HTTP/HTTPS API is a non-goal.
 | |
| //
 | |
| // Additional and modified API elements
 | |
| // ------------------------------------
 | |
| //
 | |
| // - **Class: http2.Endpoint**: an API for using the raw HTTP/2 framing layer. For documentation
 | |
| //   see the [lib/endpoint.js](endpoint.html) file.
 | |
| //
 | |
| // - **Class: http2.Server**
 | |
| //   - **Event: 'connection' (socket, [endpoint])**: there's a second argument if the negotiation of
 | |
| //     HTTP/2 was successful: the reference to the [Endpoint](endpoint.html) object tied to the
 | |
| //     socket.
 | |
| //
 | |
| // - **http2.createServer(options, [requestListener])**: additional option:
 | |
| //   - **log**: an optional [bunyan](https://github.com/trentm/node-bunyan) logger object
 | |
| //   - **plain**: if `true`, the server will accept HTTP/2 connections over plain TCP instead of
 | |
| //     TLS
 | |
| //
 | |
| // - **Class: http2.ServerResponse**
 | |
| //   - **response.push(options)**: initiates a server push. `options` describes the 'imaginary'
 | |
| //     request to which the push stream is a response; the possible options are identical to the
 | |
| //     ones accepted by `http2.request`. Returns a ServerResponse object that can be used to send
 | |
| //     the response headers and content.
 | |
| //
 | |
| // - **Class: http2.Agent**
 | |
| //   - **new Agent(options)**: additional option:
 | |
| //     - **log**: an optional [bunyan](https://github.com/trentm/node-bunyan) logger object
 | |
| //   - **agent.sockets**: only contains TCP sockets that corresponds to HTTP/1 requests.
 | |
| //   - **agent.endpoints**: contains [Endpoint](endpoint.html) objects for HTTP/2 connections.
 | |
| //
 | |
| // - **http2.request(options, [callback])**: additional option:
 | |
| //   - **plain**: if `true`, the client will not try to build a TLS tunnel, instead it will use
 | |
| //     the raw TCP stream for HTTP/2
 | |
| //
 | |
| // - **Class: http2.ClientRequest**
 | |
| //   - **Event: 'socket' (socket)**: in case of an HTTP/2 incoming message, `socket` is a reference
 | |
| //     to the associated [HTTP/2 Stream](stream.html) object (and not to the TCP socket).
 | |
| //   - **Event: 'push' (promise)**: signals the intention of a server push associated to this
 | |
| //     request. `promise` is an IncomingPromise. If there's no listener for this event, the server
 | |
| //     push is cancelled.
 | |
| //   - **request.setPriority(priority)**: assign a priority to this request. `priority` is a number
 | |
| //     between 0 (highest priority) and 2^31-1 (lowest priority). Default value is 2^30.
 | |
| //
 | |
| // - **Class: http2.IncomingMessage**
 | |
| //   - has two subclasses for easier interface description: **IncomingRequest** and
 | |
| //     **IncomingResponse**
 | |
| //   - **message.socket**: in case of an HTTP/2 incoming message, it's a reference to the associated
 | |
| //     [HTTP/2 Stream](stream.html) object (and not to the TCP socket).
 | |
| //
 | |
| // - **Class: http2.IncomingRequest (IncomingMessage)**
 | |
| //   - **message.url**: in case of an HTTP/2 incoming request, the `url` field always contains the
 | |
| //     path, and never a full url (it contains the path in most cases in the HTTPS api as well).
 | |
| //   - **message.scheme**: additional field. Mandatory HTTP/2 request metadata.
 | |
| //   - **message.host**: additional field. Mandatory HTTP/2 request metadata. Note that this
 | |
| //     replaces the old Host header field, but node-http2 will add Host to the `message.headers` for
 | |
| //     backwards compatibility.
 | |
| //
 | |
| // - **Class: http2.IncomingPromise (IncomingRequest)**
 | |
| //   - contains the metadata of the 'imaginary' request to which the server push is an answer.
 | |
| //   - **Event: 'response' (response)**: signals the arrival of the actual push stream. `response`
 | |
| //     is an IncomingResponse.
 | |
| //   - **Event: 'push' (promise)**: signals the intention of a server push associated to this
 | |
| //     request. `promise` is an IncomingPromise. If there's no listener for this event, the server
 | |
| //     push is cancelled.
 | |
| //   - **promise.cancel()**: cancels the promised server push.
 | |
| //   - **promise.setPriority(priority)**: assign a priority to this push stream. `priority` is a
 | |
| //     number between 0 (highest priority) and 2^31-1 (lowest priority). Default value is 2^30.
 | |
| //
 | |
| // API elements not yet implemented
 | |
| // --------------------------------
 | |
| //
 | |
| // - **Class: http2.Server**
 | |
| //   - **server.maxHeadersCount**
 | |
| //
 | |
| // API elements that are not applicable to HTTP/2
 | |
| // ----------------------------------------------
 | |
| //
 | |
| // The reason may be deprecation of certain HTTP/1.1 features, or that some API elements simply
 | |
| // don't make sense when using HTTP/2. These will not be present when a request is done with HTTP/2,
 | |
| // but will function normally when falling back to using HTTP/1.1.
 | |
| //
 | |
| // - **Class: http2.Server**
 | |
| //   - **Event: 'checkContinue'**: not in the spec, yet (see [http-spec#18][expect-continue])
 | |
| //   - **Event: 'upgrade'**: upgrade is deprecated in HTTP/2
 | |
| //   - **Event: 'timeout'**: HTTP/2 sockets won't timeout because of application level keepalive
 | |
| //     (PING frames)
 | |
| //   - **Event: 'connect'**: not in the spec, yet (see [http-spec#230][connect])
 | |
| //   - **server.setTimeout(msecs, [callback])**
 | |
| //   - **server.timeout**
 | |
| //
 | |
| // - **Class: http2.ServerResponse**
 | |
| //   - **Event: 'close'**
 | |
| //   - **Event: 'timeout'**
 | |
| //   - **response.writeContinue()**
 | |
| //   - **response.writeHead(statusCode, [reasonPhrase], [headers])**: reasonPhrase will always be
 | |
| //     ignored since [it's not supported in HTTP/2][3]
 | |
| //   - **response.setTimeout(timeout, [callback])**
 | |
| //
 | |
| // - **Class: http2.Agent**
 | |
| //   - **agent.maxSockets**: only affects HTTP/1 connection pool. When using HTTP/2, there's always
 | |
| //     one connection per host.
 | |
| //
 | |
| // - **Class: http2.ClientRequest**
 | |
| //   - **Event: 'upgrade'**
 | |
| //   - **Event: 'connect'**
 | |
| //   - **Event: 'continue'**
 | |
| //   - **request.setTimeout(timeout, [callback])**
 | |
| //   - **request.setNoDelay([noDelay])**
 | |
| //   - **request.setSocketKeepAlive([enable], [initialDelay])**
 | |
| //
 | |
| // - **Class: http2.IncomingMessage**
 | |
| //   - **Event: 'close'**
 | |
| //   - **message.setTimeout(timeout, [callback])**
 | |
| //
 | |
| // [1]: http://nodejs.org/api/https.html
 | |
| // [2]: http://nodejs.org/api/http.html
 | |
| // [3]: http://tools.ietf.org/html/draft-ietf-httpbis-http2-16#section-8.1.2.4
 | |
| // [expect-continue]: https://github.com/http2/http2-spec/issues/18
 | |
| // [connect]: https://github.com/http2/http2-spec/issues/230
 | |
| 
 | |
| // Common server and client side code
 | |
| // ==================================
 | |
| 
 | |
| var net = require('net');
 | |
| var url = require('url');
 | |
| var util = require('util');
 | |
| var EventEmitter = require('events').EventEmitter;
 | |
| var PassThrough = require('stream').PassThrough;
 | |
| var Readable = require('stream').Readable;
 | |
| var Writable = require('stream').Writable;
 | |
| var protocol = require('./protocol');
 | |
| var Endpoint = protocol.Endpoint;
 | |
| var http = require('http');
 | |
| var https = require('https');
 | |
| 
 | |
| exports.STATUS_CODES = http.STATUS_CODES;
 | |
| exports.IncomingMessage = IncomingMessage;
 | |
| exports.OutgoingMessage = OutgoingMessage;
 | |
| exports.protocol = protocol;
 | |
| 
 | |
| var deprecatedHeaders = [
 | |
|   'connection',
 | |
|   'host',
 | |
|   'keep-alive',
 | |
|   'proxy-connection',
 | |
|   'te',
 | |
|   'transfer-encoding',
 | |
|   'upgrade'
 | |
| ];
 | |
| 
 | |
| // When doing NPN/ALPN negotiation, HTTP/1.1 is used as fallback
 | |
| var supportedProtocols = [protocol.VERSION, 'http/1.1', 'http/1.0'];
 | |
| 
 | |
| // Ciphersuite list based on the recommendations of http://wiki.mozilla.org/Security/Server_Side_TLS
 | |
| // The only modification is that kEDH+AESGCM were placed after DHE and ECDHE suites
 | |
| var cipherSuites = [
 | |
|   'ECDHE-RSA-AES128-GCM-SHA256',
 | |
|   'ECDHE-ECDSA-AES128-GCM-SHA256',
 | |
|   'ECDHE-RSA-AES256-GCM-SHA384',
 | |
|   'ECDHE-ECDSA-AES256-GCM-SHA384',
 | |
|   'DHE-RSA-AES128-GCM-SHA256',
 | |
|   'DHE-DSS-AES128-GCM-SHA256',
 | |
|   'ECDHE-RSA-AES128-SHA256',
 | |
|   'ECDHE-ECDSA-AES128-SHA256',
 | |
|   'ECDHE-RSA-AES128-SHA',
 | |
|   'ECDHE-ECDSA-AES128-SHA',
 | |
|   'ECDHE-RSA-AES256-SHA384',
 | |
|   'ECDHE-ECDSA-AES256-SHA384',
 | |
|   'ECDHE-RSA-AES256-SHA',
 | |
|   'ECDHE-ECDSA-AES256-SHA',
 | |
|   'DHE-RSA-AES128-SHA256',
 | |
|   'DHE-RSA-AES128-SHA',
 | |
|   'DHE-DSS-AES128-SHA256',
 | |
|   'DHE-RSA-AES256-SHA256',
 | |
|   'DHE-DSS-AES256-SHA',
 | |
|   'DHE-RSA-AES256-SHA',
 | |
|   'kEDH+AESGCM',
 | |
|   'AES128-GCM-SHA256',
 | |
|   'AES256-GCM-SHA384',
 | |
|   'ECDHE-RSA-RC4-SHA',
 | |
|   'ECDHE-ECDSA-RC4-SHA',
 | |
|   'AES128',
 | |
|   'AES256',
 | |
|   'RC4-SHA',
 | |
|   'HIGH',
 | |
|   '!aNULL',
 | |
|   '!eNULL',
 | |
|   '!EXPORT',
 | |
|   '!DES',
 | |
|   '!3DES',
 | |
|   '!MD5',
 | |
|   '!PSK'
 | |
| ].join(':');
 | |
| 
 | |
| // Logging
 | |
| // -------
 | |
| 
 | |
| // Logger shim, used when no logger is provided by the user.
 | |
| function noop() {}
 | |
| var defaultLogger = {
 | |
|   fatal: noop,
 | |
|   error: noop,
 | |
|   warn : noop,
 | |
|   info : noop,
 | |
|   debug: noop,
 | |
|   trace: noop,
 | |
| 
 | |
|   child: function() { return this; }
 | |
| };
 | |
| 
 | |
| // Bunyan serializers exported by submodules that are worth adding when creating a logger.
 | |
| exports.serializers = protocol.serializers;
 | |
| 
 | |
| // IncomingMessage class
 | |
| // ---------------------
 | |
| 
 | |
| function IncomingMessage(stream) {
 | |
|   // * This is basically a read-only wrapper for the [Stream](stream.html) class.
 | |
|   PassThrough.call(this);
 | |
|   stream.pipe(this);
 | |
|   this.socket = this.stream = stream;
 | |
| 
 | |
|   this._log = stream._log.child({ component: 'http' });
 | |
| 
 | |
|   // * HTTP/2.0 does not define a way to carry the version identifier that is included in the
 | |
|   //   HTTP/1.1 request/status line. Version is always 2.0.
 | |
|   this.httpVersion = '2.0';
 | |
|   this.httpVersionMajor = 2;
 | |
|   this.httpVersionMinor = 0;
 | |
| 
 | |
|   // * `this.headers` will store the regular headers (and none of the special colon headers)
 | |
|   this.headers = {};
 | |
|   this.trailers = undefined;
 | |
|   this._lastHeadersSeen = undefined;
 | |
| 
 | |
|   // * Other metadata is filled in when the headers arrive.
 | |
|   stream.once('headers', this._onHeaders.bind(this));
 | |
|   stream.once('end', this._onEnd.bind(this));
 | |
| }
 | |
| IncomingMessage.prototype = Object.create(PassThrough.prototype, { constructor: { value: IncomingMessage } });
 | |
| 
 | |
| // [Request Header Fields](http://tools.ietf.org/html/draft-ietf-httpbis-http2-16#section-8.1.2.3)
 | |
| // * `headers` argument: HTTP/2.0 request and response header fields carry information as a series
 | |
| //   of key-value pairs. This includes the target URI for the request, the status code for the
 | |
| //   response, as well as HTTP header fields.
 | |
| IncomingMessage.prototype._onHeaders = function _onHeaders(headers) {
 | |
|   // * Detects malformed headers
 | |
|   this._validateHeaders(headers);
 | |
| 
 | |
|   // * Store the _regular_ headers in `this.headers`
 | |
|   for (var name in headers) {
 | |
|     if (name[0] !== ':') {
 | |
|       this.headers[name] = headers[name];
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   // * The last header block, if it's not the first, will represent the trailers
 | |
|   var self = this;
 | |
|   this.stream.on('headers', function(headers) {
 | |
|     self._lastHeadersSeen = headers;
 | |
|   });
 | |
| };
 | |
| 
 | |
| IncomingMessage.prototype._onEnd = function _onEnd() {
 | |
|   this.trailers = this._lastHeadersSeen;
 | |
| };
 | |
| 
 | |
| IncomingMessage.prototype.setTimeout = noop;
 | |
| 
 | |
| IncomingMessage.prototype._checkSpecialHeader = function _checkSpecialHeader(key, value) {
 | |
|   if ((typeof value !== 'string') || (value.length === 0)) {
 | |
|     this._log.error({ key: key, value: value }, 'Invalid or missing special header field');
 | |
|     this.stream.reset('PROTOCOL_ERROR');
 | |
|   }
 | |
| 
 | |
|   return value;
 | |
| };
 | |
| 
 | |
| IncomingMessage.prototype._validateHeaders = function _validateHeaders(headers) {
 | |
|   // * An HTTP/2.0 request or response MUST NOT include any of the following header fields:
 | |
|   //   Connection, Host, Keep-Alive, Proxy-Connection, TE, Transfer-Encoding, and Upgrade. A server
 | |
|   //   MUST treat the presence of any of these header fields as a stream error of type
 | |
|   //   PROTOCOL_ERROR.
 | |
|   for (var i = 0; i < deprecatedHeaders.length; i++) {
 | |
|     var key = deprecatedHeaders[i];
 | |
|     if (key in headers) {
 | |
|       this._log.error({ key: key, value: headers[key] }, 'Deprecated header found');
 | |
|       this.stream.reset('PROTOCOL_ERROR');
 | |
|       return;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   for (var headerName in headers) {
 | |
|     // * Empty header name field is malformed
 | |
|     if (headerName.length <= 1) {
 | |
|       this.stream.reset('PROTOCOL_ERROR');
 | |
|       return;
 | |
|     }
 | |
|     // * A request or response containing uppercase header name field names MUST be
 | |
|     //   treated as malformed (Section 8.1.3.5). Implementations that detect malformed
 | |
|     //   requests or responses need to ensure that the stream ends.
 | |
|     if(/[A-Z]/.test(headerName)) {
 | |
|       this.stream.reset('PROTOCOL_ERROR');
 | |
|       return;
 | |
|     }
 | |
|   }
 | |
| };
 | |
| 
 | |
| // OutgoingMessage class
 | |
| // ---------------------
 | |
| 
 | |
| function OutgoingMessage() {
 | |
|   // * This is basically a read-only wrapper for the [Stream](stream.html) class.
 | |
|   Writable.call(this);
 | |
| 
 | |
|   this._headers = {};
 | |
|   this._trailers = undefined;
 | |
|   this.headersSent = false;
 | |
| 
 | |
|   this.on('finish', this._finish);
 | |
| }
 | |
| OutgoingMessage.prototype = Object.create(Writable.prototype, { constructor: { value: OutgoingMessage } });
 | |
| 
 | |
| OutgoingMessage.prototype._write = function _write(chunk, encoding, callback) {
 | |
|   if (this.stream) {
 | |
|     this.stream.write(chunk, encoding, callback);
 | |
|   } else {
 | |
|     this.once('socket', this._write.bind(this, chunk, encoding, callback));
 | |
|   }
 | |
| };
 | |
| 
 | |
| OutgoingMessage.prototype._finish = function _finish() {
 | |
|   if (this.stream) {
 | |
|     if (this._trailers) {
 | |
|       if (this.request) {
 | |
|         this.request.addTrailers(this._trailers);
 | |
|       } else {
 | |
|         this.stream.headers(this._trailers);
 | |
|       }
 | |
|     }
 | |
|     this.stream.end();
 | |
|   } else {
 | |
|     this.once('socket', this._finish.bind(this));
 | |
|   }
 | |
| };
 | |
| 
 | |
| OutgoingMessage.prototype.setHeader = function setHeader(name, value) {
 | |
|   if (this.headersSent) {
 | |
|     throw new Error('Can\'t set headers after they are sent.');
 | |
|   } else {
 | |
|     name = name.toLowerCase();
 | |
|     if (deprecatedHeaders.indexOf(name) !== -1) {
 | |
|       throw new Error('Cannot set deprecated header: ' + name);
 | |
|     }
 | |
|     this._headers[name] = value;
 | |
|   }
 | |
| };
 | |
| 
 | |
| OutgoingMessage.prototype.removeHeader = function removeHeader(name) {
 | |
|   if (this.headersSent) {
 | |
|     throw new Error('Can\'t remove headers after they are sent.');
 | |
|   } else {
 | |
|     delete this._headers[name.toLowerCase()];
 | |
|   }
 | |
| };
 | |
| 
 | |
| OutgoingMessage.prototype.getHeader = function getHeader(name) {
 | |
|   return this._headers[name.toLowerCase()];
 | |
| };
 | |
| 
 | |
| OutgoingMessage.prototype.addTrailers = function addTrailers(trailers) {
 | |
|   this._trailers = trailers;
 | |
| };
 | |
| 
 | |
| OutgoingMessage.prototype.setTimeout = noop;
 | |
| 
 | |
| OutgoingMessage.prototype._checkSpecialHeader = IncomingMessage.prototype._checkSpecialHeader;
 | |
| 
 | |
| // Server side
 | |
| // ===========
 | |
| 
 | |
| exports.Server = Server;
 | |
| exports.IncomingRequest = IncomingRequest;
 | |
| exports.OutgoingResponse = OutgoingResponse;
 | |
| exports.ServerResponse = OutgoingResponse; // for API compatibility
 | |
| 
 | |
| // Server class
 | |
| // ------------
 | |
| 
 | |
| function Server(options) {
 | |
|   options = util._extend({}, options);
 | |
| 
 | |
|   this._log = (options.log || defaultLogger).child({ component: 'http' });
 | |
|   this._settings = options.settings;
 | |
| 
 | |
|   var start = this._start.bind(this);
 | |
|   var fallback = this._fallback.bind(this);
 | |
| 
 | |
|   // HTTP2 over TLS (using NPN or ALPN)
 | |
|   if ((options.key && options.cert) || options.pfx) {
 | |
|     this._log.info('Creating HTTP/2 server over TLS');
 | |
|     this._mode = 'tls';
 | |
|     options.ALPNProtocols = supportedProtocols;
 | |
|     options.NPNProtocols = supportedProtocols;
 | |
|     options.ciphers = options.ciphers || cipherSuites;
 | |
|     options.honorCipherOrder = (options.honorCipherOrder != false);
 | |
|     this._server = https.createServer(options);
 | |
|     this._originalSocketListeners = this._server.listeners('secureConnection');
 | |
|     this._server.removeAllListeners('secureConnection');
 | |
|     this._server.on('secureConnection', function(socket) {
 | |
|       var negotiatedProtocol = socket.alpnProtocol || socket.npnProtocol;
 | |
|       if ((negotiatedProtocol === protocol.VERSION) && socket.servername) {
 | |
|         start(socket);
 | |
|       } else {
 | |
|         fallback(socket);
 | |
|       }
 | |
|     });
 | |
|     this._server.on('request', this.emit.bind(this, 'request'));
 | |
|   }
 | |
| 
 | |
|   // HTTP2 over plain TCP
 | |
|   else if (options.plain) {
 | |
|     this._log.info('Creating HTTP/2 server over plain TCP');
 | |
|     this._mode = 'plain';
 | |
|     this._server = net.createServer(start);
 | |
|   }
 | |
| 
 | |
|   // HTTP/2 with HTTP/1.1 upgrade
 | |
|   else {
 | |
|     this._log.error('Trying to create HTTP/2 server with Upgrade from HTTP/1.1');
 | |
|     throw new Error('HTTP1.1 -> HTTP2 upgrade is not yet supported. Please provide TLS keys.');
 | |
|   }
 | |
| 
 | |
|   this._server.on('close', this.emit.bind(this, 'close'));
 | |
| }
 | |
| Server.prototype = Object.create(EventEmitter.prototype, { constructor: { value: Server } });
 | |
| 
 | |
| // Starting HTTP/2
 | |
| Server.prototype._start = function _start(socket) {
 | |
|   var endpoint = new Endpoint(this._log, 'SERVER', this._settings);
 | |
| 
 | |
|   this._log.info({ e: endpoint,
 | |
|                    client: socket.remoteAddress + ':' + socket.remotePort,
 | |
|                    SNI: socket.servername
 | |
|                  }, 'New incoming HTTP/2 connection');
 | |
| 
 | |
|   endpoint.pipe(socket).pipe(endpoint);
 | |
| 
 | |
|   var self = this;
 | |
|   endpoint.on('stream', function _onStream(stream) {
 | |
|     var response = new OutgoingResponse(stream);
 | |
|     var request = new IncomingRequest(stream);
 | |
| 
 | |
|     request.once('ready', self.emit.bind(self, 'request', request, response));
 | |
|   });
 | |
| 
 | |
|   endpoint.on('error', this.emit.bind(this, 'clientError'));
 | |
|   socket.on('error', this.emit.bind(this, 'clientError'));
 | |
| 
 | |
|   this.emit('connection', socket, endpoint);
 | |
| };
 | |
| 
 | |
| Server.prototype._fallback = function _fallback(socket) {
 | |
|   var negotiatedProtocol = socket.alpnProtocol || socket.npnProtocol;
 | |
| 
 | |
|   this._log.info({ client: socket.remoteAddress + ':' + socket.remotePort,
 | |
|                    protocol: negotiatedProtocol,
 | |
|                    SNI: socket.servername
 | |
|                  }, 'Falling back to simple HTTPS');
 | |
| 
 | |
|   for (var i = 0; i < this._originalSocketListeners.length; i++) {
 | |
|     this._originalSocketListeners[i].call(this._server, socket);
 | |
|   }
 | |
| 
 | |
|   this.emit('connection', socket);
 | |
| };
 | |
| 
 | |
| // There are [3 possible signatures][1] of the `listen` function. Every arguments is forwarded to
 | |
| // the backing TCP or HTTPS server.
 | |
| // [1]: http://nodejs.org/api/http.html#http_server_listen_port_hostname_backlog_callback
 | |
| Server.prototype.listen = function listen(port, hostname) {
 | |
|   this._log.info({ on: ((typeof hostname === 'string') ? (hostname + ':' + port) : port) },
 | |
|                  'Listening for incoming connections');
 | |
|   this._server.listen.apply(this._server, arguments);
 | |
| };
 | |
| 
 | |
| Server.prototype.close = function close(callback) {
 | |
|   this._log.info('Closing server');
 | |
|   this._server.close(callback);
 | |
| };
 | |
| 
 | |
| Server.prototype.setTimeout = function setTimeout(timeout, callback) {
 | |
|   if (this._mode === 'tls') {
 | |
|     this._server.setTimeout(timeout, callback);
 | |
|   }
 | |
| };
 | |
| 
 | |
| Object.defineProperty(Server.prototype, 'timeout', {
 | |
|   get: function getTimeout() {
 | |
|     if (this._mode === 'tls') {
 | |
|       return this._server.timeout;
 | |
|     } else {
 | |
|       return undefined;
 | |
|     }
 | |
|   },
 | |
|   set: function setTimeout(timeout) {
 | |
|     if (this._mode === 'tls') {
 | |
|       this._server.timeout = timeout;
 | |
|     }
 | |
|   }
 | |
| });
 | |
| 
 | |
| // Overriding `EventEmitter`'s `on(event, listener)` method to forward certain subscriptions to
 | |
| // `server`.There are events on the `http.Server` class where it makes difference whether someone is
 | |
| // listening on the event or not. In these cases, we can not simply forward the events from the
 | |
| // `server` to `this` since that means a listener. Instead, we forward the subscriptions.
 | |
| Server.prototype.on = function on(event, listener) {
 | |
|   if ((event === 'upgrade') || (event === 'timeout')) {
 | |
|     this._server.on(event, listener && listener.bind(this));
 | |
|   } else {
 | |
|     EventEmitter.prototype.on.call(this, event, listener);
 | |
|   }
 | |
| };
 | |
| 
 | |
| // `addContext` is used to add Server Name Indication contexts
 | |
| Server.prototype.addContext = function addContext(hostname, credentials) {
 | |
|   if (this._mode === 'tls') {
 | |
|     this._server.addContext(hostname, credentials);
 | |
|   }
 | |
| };
 | |
| 
 | |
| function createServerRaw(options, requestListener) {
 | |
|   if (typeof options === 'function') {
 | |
|     requestListener = options;
 | |
|     options = {};
 | |
|   }
 | |
| 
 | |
|   if (options.pfx || (options.key && options.cert)) {
 | |
|     throw new Error('options.pfx, options.key, and options.cert are nonsensical!');
 | |
|   }
 | |
| 
 | |
|   options.plain = true;
 | |
|   var server = new Server(options);
 | |
| 
 | |
|   if (requestListener) {
 | |
|     server.on('request', requestListener);
 | |
|   }
 | |
| 
 | |
|   return server;
 | |
| }
 | |
| 
 | |
| function createServerTLS(options, requestListener) {
 | |
|   if (typeof options === 'function') {
 | |
|     throw new Error('options are required!');
 | |
|   }
 | |
|   if (!options.pfx && !(options.key && options.cert)) {
 | |
|     throw new Error('options.pfx or options.key and options.cert are required!');
 | |
|   }
 | |
|   options.plain = false;
 | |
| 
 | |
|   var server = new Server(options);
 | |
| 
 | |
|   if (requestListener) {
 | |
|     server.on('request', requestListener);
 | |
|   }
 | |
| 
 | |
|   return server;
 | |
| }
 | |
| 
 | |
| // Exposed main interfaces for HTTPS connections (the default)
 | |
| exports.https = {};
 | |
| exports.createServer = exports.https.createServer = createServerTLS;
 | |
| exports.request = exports.https.request = requestTLS;
 | |
| exports.get = exports.https.get = getTLS;
 | |
| 
 | |
| // Exposed main interfaces for raw TCP connections (not recommended)
 | |
| exports.raw = {};
 | |
| exports.raw.createServer = createServerRaw;
 | |
| exports.raw.request = requestRaw;
 | |
| exports.raw.get = getRaw;
 | |
| 
 | |
| // Exposed main interfaces for HTTP plaintext upgrade connections (not implemented)
 | |
| function notImplemented() {
 | |
|     throw new Error('HTTP UPGRADE is not implemented!');
 | |
| }
 | |
| 
 | |
| exports.http = {};
 | |
| exports.http.createServer = exports.http.request = exports.http.get = notImplemented;
 | |
| 
 | |
| // IncomingRequest class
 | |
| // ---------------------
 | |
| 
 | |
| function IncomingRequest(stream) {
 | |
|   IncomingMessage.call(this, stream);
 | |
| }
 | |
| IncomingRequest.prototype = Object.create(IncomingMessage.prototype, { constructor: { value: IncomingRequest } });
 | |
| 
 | |
| // [Request Header Fields](http://tools.ietf.org/html/draft-ietf-httpbis-http2-16#section-8.1.2.3)
 | |
| // * `headers` argument: HTTP/2.0 request and response header fields carry information as a series
 | |
| //   of key-value pairs. This includes the target URI for the request, the status code for the
 | |
| //   response, as well as HTTP header fields.
 | |
| IncomingRequest.prototype._onHeaders = function _onHeaders(headers) {
 | |
|   // * The ":method" header field includes the HTTP method
 | |
|   // * The ":scheme" header field includes the scheme portion of the target URI
 | |
|   // * The ":authority" header field includes the authority portion of the target URI
 | |
|   // * The ":path" header field includes the path and query parts of the target URI.
 | |
|   //   This field MUST NOT be empty; URIs that do not contain a path component MUST include a value
 | |
|   //   of '/', unless the request is an OPTIONS request for '*', in which case the ":path" header
 | |
|   //   field MUST include '*'.
 | |
|   // * All HTTP/2.0 requests MUST include exactly one valid value for all of these header fields. A
 | |
|   //   server MUST treat the absence of any of these header fields, presence of multiple values, or
 | |
|   //   an invalid value as a stream error of type PROTOCOL_ERROR.
 | |
|   this.method = this._checkSpecialHeader(':method'   , headers[':method']);
 | |
|   this.scheme = this._checkSpecialHeader(':scheme'   , headers[':scheme']);
 | |
|   this.host   = this._checkSpecialHeader(':authority', headers[':authority']  );
 | |
|   this.url    = this._checkSpecialHeader(':path'     , headers[':path']  );
 | |
| 
 | |
|   // * Host header is included in the headers object for backwards compatibility.
 | |
|   this.headers.host = this.host;
 | |
| 
 | |
|   // * Handling regular headers.
 | |
|   IncomingMessage.prototype._onHeaders.call(this, headers);
 | |
| 
 | |
|   // * Signaling that the headers arrived.
 | |
|   this._log.info({ method: this.method, scheme: this.scheme, host: this.host,
 | |
|                    path: this.url, headers: this.headers }, 'Incoming request');
 | |
|   this.emit('ready');
 | |
| };
 | |
| 
 | |
| // OutgoingResponse class
 | |
| // ----------------------
 | |
| 
 | |
| function OutgoingResponse(stream) {
 | |
|   OutgoingMessage.call(this);
 | |
| 
 | |
|   this._log = stream._log.child({ component: 'http' });
 | |
| 
 | |
|   this.stream = stream;
 | |
|   this.statusCode = 200;
 | |
|   this.sendDate = true;
 | |
| 
 | |
|   this.stream.once('headers', this._onRequestHeaders.bind(this));
 | |
| }
 | |
| OutgoingResponse.prototype = Object.create(OutgoingMessage.prototype, { constructor: { value: OutgoingResponse } });
 | |
| 
 | |
| OutgoingResponse.prototype.writeHead = function writeHead(statusCode, reasonPhrase, headers) {
 | |
|   if (this.headersSent) {
 | |
|     return;
 | |
|   }
 | |
| 
 | |
|   if (typeof reasonPhrase === 'string') {
 | |
|     this._log.warn('Reason phrase argument was present but ignored by the writeHead method');
 | |
|   } else {
 | |
|     headers = reasonPhrase;
 | |
|   }
 | |
| 
 | |
|   for (var name in headers) {
 | |
|     this.setHeader(name, headers[name]);
 | |
|   }
 | |
|   headers = this._headers;
 | |
| 
 | |
|   if (this.sendDate && !('date' in this._headers)) {
 | |
|     headers.date = (new Date()).toUTCString();
 | |
|   }
 | |
| 
 | |
|   this._log.info({ status: statusCode, headers: this._headers }, 'Sending server response');
 | |
| 
 | |
|   headers[':status'] = this.statusCode = statusCode;
 | |
| 
 | |
|   this.stream.headers(headers);
 | |
|   this.headersSent = true;
 | |
| };
 | |
| 
 | |
| OutgoingResponse.prototype._implicitHeaders = function _implicitHeaders() {
 | |
|   if (!this.headersSent) {
 | |
|     this.writeHead(this.statusCode);
 | |
|   }
 | |
| };
 | |
| 
 | |
| OutgoingResponse.prototype.write = function write() {
 | |
|   this._implicitHeaders();
 | |
|   return OutgoingMessage.prototype.write.apply(this, arguments);
 | |
| };
 | |
| 
 | |
| OutgoingResponse.prototype.end = function end() {
 | |
|   this._implicitHeaders();
 | |
|   return OutgoingMessage.prototype.end.apply(this, arguments);
 | |
| };
 | |
| 
 | |
| OutgoingResponse.prototype._onRequestHeaders = function _onRequestHeaders(headers) {
 | |
|   this._requestHeaders = headers;
 | |
| };
 | |
| 
 | |
| OutgoingResponse.prototype.push = function push(options) {
 | |
|   if (typeof options === 'string') {
 | |
|     options = url.parse(options);
 | |
|   }
 | |
| 
 | |
|   if (!options.path) {
 | |
|     throw new Error('`path` option is mandatory.');
 | |
|   }
 | |
| 
 | |
|   var promise = util._extend({
 | |
|     ':method': (options.method || 'GET').toUpperCase(),
 | |
|     ':scheme': (options.protocol && options.protocol.slice(0, -1)) || this._requestHeaders[':scheme'],
 | |
|     ':authority': options.hostname || options.host || this._requestHeaders[':authority'],
 | |
|     ':path': options.path
 | |
|   }, options.headers);
 | |
| 
 | |
|   this._log.info({ method: promise[':method'], scheme: promise[':scheme'],
 | |
|                    authority: promise[':authority'], path: promise[':path'],
 | |
|                    headers: options.headers }, 'Promising push stream');
 | |
| 
 | |
|   var pushStream = this.stream.promise(promise);
 | |
| 
 | |
|   return new OutgoingResponse(pushStream);
 | |
| };
 | |
| 
 | |
| OutgoingResponse.prototype.altsvc = function altsvc(host, port, protocolID, maxAge, origin) {
 | |
|     if (origin === undefined) {
 | |
|         origin = "";
 | |
|     }
 | |
|     this.stream.altsvc(host, port, protocolID, maxAge, origin);
 | |
| };
 | |
| 
 | |
| // Overriding `EventEmitter`'s `on(event, listener)` method to forward certain subscriptions to
 | |
| // `request`. See `Server.prototype.on` for explanation.
 | |
| OutgoingResponse.prototype.on = function on(event, listener) {
 | |
|   if (this.request && (event === 'timeout')) {
 | |
|     this.request.on(event, listener && listener.bind(this));
 | |
|   } else {
 | |
|     OutgoingMessage.prototype.on.call(this, event, listener);
 | |
|   }
 | |
| };
 | |
| 
 | |
| // Client side
 | |
| // ===========
 | |
| 
 | |
| exports.ClientRequest = OutgoingRequest; // for API compatibility
 | |
| exports.OutgoingRequest = OutgoingRequest;
 | |
| exports.IncomingResponse = IncomingResponse;
 | |
| exports.Agent = Agent;
 | |
| exports.globalAgent = undefined;
 | |
| 
 | |
| function requestRaw(options, callback) {
 | |
|   if (typeof options === "string") {
 | |
|     options = url.parse(options);
 | |
|   }
 | |
|   options.plain = true;
 | |
|   if (options.protocol && options.protocol !== "http:") {
 | |
|     throw new Error('This interface only supports http-schemed URLs');
 | |
|   }
 | |
|   return (options.agent || exports.globalAgent).request(options, callback);
 | |
| }
 | |
| 
 | |
| function requestTLS(options, callback) {
 | |
|   if (typeof options === "string") {
 | |
|     options = url.parse(options);
 | |
|   }
 | |
|   options.plain = false;
 | |
|   if (options.protocol && options.protocol !== "https:") {
 | |
|     throw new Error('This interface only supports https-schemed URLs');
 | |
|   }
 | |
|   return (options.agent || exports.globalAgent).request(options, callback);
 | |
| }
 | |
| 
 | |
| function getRaw(options, callback) {
 | |
|   if (typeof options === "string") {
 | |
|     options = url.parse(options);
 | |
|   }
 | |
|   options.plain = true;
 | |
|   if (options.protocol && options.protocol !== "http:") {
 | |
|     throw new Error('This interface only supports http-schemed URLs');
 | |
|   }
 | |
|   return (options.agent || exports.globalAgent).get(options, callback);
 | |
| }
 | |
| 
 | |
| function getTLS(options, callback) {
 | |
|   if (typeof options === "string") {
 | |
|     options = url.parse(options);
 | |
|   }
 | |
|   options.plain = false;
 | |
|   if (options.protocol && options.protocol !== "https:") {
 | |
|     throw new Error('This interface only supports https-schemed URLs');
 | |
|   }
 | |
|   return (options.agent || exports.globalAgent).get(options, callback);
 | |
| }
 | |
| 
 | |
| // Agent class
 | |
| // -----------
 | |
| 
 | |
| function Agent(options) {
 | |
|   EventEmitter.call(this);
 | |
| 
 | |
|   options = util._extend({}, options);
 | |
| 
 | |
|   this._settings = options.settings;
 | |
|   this._log = (options.log || defaultLogger).child({ component: 'http' });
 | |
|   this.endpoints = {};
 | |
| 
 | |
|   // * Using an own HTTPS agent, because the global agent does not look at `NPN/ALPNProtocols` 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.
 | |
|   var agentOptions = {};
 | |
|   agentOptions.ALPNProtocols = supportedProtocols;
 | |
|   agentOptions.NPNProtocols = supportedProtocols;
 | |
|   this._httpsAgent = new https.Agent(agentOptions);
 | |
| 
 | |
|   this.sockets = this._httpsAgent.sockets;
 | |
|   this.requests = this._httpsAgent.requests;
 | |
| }
 | |
| Agent.prototype = Object.create(EventEmitter.prototype, { constructor: { value: Agent } });
 | |
| 
 | |
| Agent.prototype.request = function request(options, callback) {
 | |
|   if (typeof options === 'string') {
 | |
|     options = url.parse(options);
 | |
|   } else {
 | |
|     options = util._extend({}, options);
 | |
|   }
 | |
| 
 | |
|   options.method = (options.method || 'GET').toUpperCase();
 | |
|   options.protocol = options.protocol || 'https:';
 | |
|   options.host = options.hostname || options.host || 'localhost';
 | |
|   options.port = options.port || 443;
 | |
|   options.path = options.path || '/';
 | |
| 
 | |
|   if (!options.plain && 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 OutgoingRequest(this._log);
 | |
| 
 | |
|   if (callback) {
 | |
|     request.on('response', callback);
 | |
|   }
 | |
| 
 | |
|   var key = [
 | |
|     !!options.plain,
 | |
|     options.host,
 | |
|     options.port
 | |
|   ].join(':');
 | |
| 
 | |
|   // * 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);
 | |
|   }
 | |
| 
 | |
|   // * HTTP/2 over plain TCP
 | |
|   else if (options.plain) {
 | |
|     endpoint = new Endpoint(this._log, 'CLIENT', this._settings);
 | |
|     endpoint.socket = net.connect({
 | |
|       host: options.host,
 | |
|       port: options.port,
 | |
|       localAddress: options.localAddress
 | |
|     });
 | |
|     endpoint.pipe(endpoint.socket).pipe(endpoint);
 | |
|     request._start(endpoint.createStream(), options);
 | |
|   }
 | |
| 
 | |
|   // * HTTP/2 over TLS negotiated using NPN or ALPN, or fallback to HTTPS1
 | |
|   else {
 | |
|     var started = false;
 | |
|     options.ALPNProtocols = supportedProtocols;
 | |
|     options.NPNProtocols = supportedProtocols;
 | |
|     options.servername = options.host; // Server Name Indication
 | |
|     options.agent = this._httpsAgent;
 | |
|     options.ciphers = options.ciphers || cipherSuites;
 | |
|     var httpsRequest = https.request(options);
 | |
| 
 | |
|     httpsRequest.on('socket', function(socket) {
 | |
|       var negotiatedProtocol = socket.alpnProtocol || socket.npnProtocol;
 | |
|       if (negotiatedProtocol != null) { // null in >=0.11.0, undefined in <0.11.0
 | |
|         negotiated();
 | |
|       } else {
 | |
|         socket.on('secureConnect', negotiated);
 | |
|       }
 | |
|     });
 | |
| 
 | |
|     var self = this;
 | |
|     function negotiated() {
 | |
|       var endpoint;
 | |
|       var negotiatedProtocol = httpsRequest.socket.alpnProtocol || httpsRequest.socket.npnProtocol;
 | |
|       if (negotiatedProtocol === protocol.VERSION) {
 | |
|         httpsRequest.socket.emit('agentRemove');
 | |
|         unbundleSocket(httpsRequest.socket);
 | |
|         endpoint = new Endpoint(self._log, 'CLIENT', self._settings);
 | |
|         endpoint.socket = httpsRequest.socket;
 | |
|         endpoint.pipe(endpoint.socket).pipe(endpoint);
 | |
|       }
 | |
|       if (started) {
 | |
|         // ** In the meantime, an other connection was made to the same host...
 | |
|         if (endpoint) {
 | |
|           // *** and it turned out to be HTTP2 and the request was multiplexed on that one, so we should close this one
 | |
|           endpoint.close();
 | |
|         }
 | |
|         // *** otherwise, the fallback to HTTPS1 is already done.
 | |
|       } else {
 | |
|         if (endpoint) {
 | |
|           self._log.info({ e: endpoint, server: options.host + ':' + options.port },
 | |
|                          'New outgoing HTTP/2 connection');
 | |
|           self.endpoints[key] = endpoint;
 | |
|           self.emit(key, endpoint);
 | |
|         } else {
 | |
|           self.emit(key, undefined);
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     this.once(key, function(endpoint) {
 | |
|       started = true;
 | |
|       if (endpoint) {
 | |
|         request._start(endpoint.createStream(), options);
 | |
|       } else {
 | |
|         request._fallback(httpsRequest);
 | |
|       }
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   return request;
 | |
| };
 | |
| 
 | |
| Agent.prototype.get = function get(options, callback) {
 | |
|   var request = this.request(options, callback);
 | |
|   request.end();
 | |
|   return request;
 | |
| };
 | |
| 
 | |
| 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;
 | |
| }
 | |
| 
 | |
| Object.defineProperty(Agent.prototype, 'maxSockets', {
 | |
|   get: function getMaxSockets() {
 | |
|     return this._httpsAgent.maxSockets;
 | |
|   },
 | |
|   set: function setMaxSockets(value) {
 | |
|     this._httpsAgent.maxSockets = value;
 | |
|   }
 | |
| });
 | |
| 
 | |
| exports.globalAgent = new Agent();
 | |
| 
 | |
| // OutgoingRequest class
 | |
| // ---------------------
 | |
| 
 | |
| function OutgoingRequest() {
 | |
|   OutgoingMessage.call(this);
 | |
| 
 | |
|   this._log = undefined;
 | |
| 
 | |
|   this.stream = undefined;
 | |
| }
 | |
| OutgoingRequest.prototype = Object.create(OutgoingMessage.prototype, { constructor: { value: OutgoingRequest } });
 | |
| 
 | |
| OutgoingRequest.prototype._start = function _start(stream, options) {
 | |
|   this.stream = stream;
 | |
| 
 | |
|   this._log = stream._log.child({ component: 'http' });
 | |
| 
 | |
|   for (var key in options.headers) {
 | |
|     this.setHeader(key, options.headers[key]);
 | |
|   }
 | |
|   var headers = this._headers;
 | |
|   delete headers.host;
 | |
| 
 | |
|   if (options.auth) {
 | |
|     headers.authorization = 'Basic ' + new Buffer(options.auth).toString('base64');
 | |
|   }
 | |
| 
 | |
|   headers[':scheme'] = options.protocol.slice(0, -1);
 | |
|   headers[':method'] = options.method;
 | |
|   headers[':authority'] = options.host;
 | |
|   headers[':path'] = options.path;
 | |
| 
 | |
|   this._log.info({ scheme: headers[':scheme'], method: headers[':method'],
 | |
|                    authority: headers[':authority'], path: headers[':path'],
 | |
|                    headers: (options.headers || {}) }, 'Sending request');
 | |
|   this.stream.headers(headers);
 | |
|   this.headersSent = true;
 | |
| 
 | |
|   this.emit('socket', this.stream);
 | |
| 
 | |
|   var response = new IncomingResponse(this.stream);
 | |
|   response.once('ready', this.emit.bind(this, 'response', response));
 | |
| 
 | |
|   this.stream.on('promise', this._onPromise.bind(this));
 | |
| };
 | |
| 
 | |
| OutgoingRequest.prototype._fallback = function _fallback(request) {
 | |
|   request.on('response', this.emit.bind(this, 'response'));
 | |
|   this.stream = this.request = request;
 | |
|   this.emit('socket', this.socket);
 | |
| };
 | |
| 
 | |
| OutgoingRequest.prototype.setPriority = function setPriority(priority) {
 | |
|   if (this.stream) {
 | |
|     this.stream.priority(priority);
 | |
|   } else {
 | |
|     this.once('socket', this.setPriority.bind(this, priority));
 | |
|   }
 | |
| };
 | |
| 
 | |
| // Overriding `EventEmitter`'s `on(event, listener)` method to forward certain subscriptions to
 | |
| // `request`. See `Server.prototype.on` for explanation.
 | |
| OutgoingRequest.prototype.on = function on(event, listener) {
 | |
|   if (this.request && (event === 'upgrade')) {
 | |
|     this.request.on(event, listener && listener.bind(this));
 | |
|   } else {
 | |
|     OutgoingMessage.prototype.on.call(this, event, listener);
 | |
|   }
 | |
| };
 | |
| 
 | |
| // Methods only in fallback mode
 | |
| OutgoingRequest.prototype.setNoDelay = function setNoDelay(noDelay) {
 | |
|   if (this.request) {
 | |
|     this.request.setNoDelay(noDelay);
 | |
|   } else if (!this.stream) {
 | |
|     this.on('socket', this.setNoDelay.bind(this, noDelay));
 | |
|   }
 | |
| };
 | |
| 
 | |
| OutgoingRequest.prototype.setSocketKeepAlive = function setSocketKeepAlive(enable, initialDelay) {
 | |
|   if (this.request) {
 | |
|     this.request.setSocketKeepAlive(enable, initialDelay);
 | |
|   } else if (!this.stream) {
 | |
|     this.on('socket', this.setSocketKeepAlive.bind(this, enable, initialDelay));
 | |
|   }
 | |
| };
 | |
| 
 | |
| OutgoingRequest.prototype.setTimeout = function setTimeout(timeout, callback) {
 | |
|   if (this.request) {
 | |
|     this.request.setTimeout(timeout, callback);
 | |
|   } else if (!this.stream) {
 | |
|     this.on('socket', this.setTimeout.bind(this, timeout, callback));
 | |
|   }
 | |
| };
 | |
| 
 | |
| // Aborting the request
 | |
| OutgoingRequest.prototype.abort = function abort() {
 | |
|   if (this.request) {
 | |
|     this.request.abort();
 | |
|   } else if (this.stream) {
 | |
|     this.stream.reset('CANCEL');
 | |
|   } else {
 | |
|     this.on('socket', this.abort.bind(this));
 | |
|   }
 | |
| };
 | |
| 
 | |
| // Receiving push promises
 | |
| OutgoingRequest.prototype._onPromise = function _onPromise(stream, headers) {
 | |
|   this._log.info({ push_stream: stream.id }, 'Receiving push promise');
 | |
| 
 | |
|   var promise = new IncomingPromise(stream, headers);
 | |
| 
 | |
|   if (this.listeners('push').length > 0) {
 | |
|     this.emit('push', promise);
 | |
|   } else {
 | |
|     promise.cancel();
 | |
|   }
 | |
| };
 | |
| 
 | |
| // IncomingResponse class
 | |
| // ----------------------
 | |
| 
 | |
| function IncomingResponse(stream) {
 | |
|   IncomingMessage.call(this, stream);
 | |
| }
 | |
| IncomingResponse.prototype = Object.create(IncomingMessage.prototype, { constructor: { value: IncomingResponse } });
 | |
| 
 | |
| // [Response Header Fields](http://tools.ietf.org/html/draft-ietf-httpbis-http2-16#section-8.1.2.4)
 | |
| // * `headers` argument: HTTP/2.0 request and response header fields carry information as a series
 | |
| //   of key-value pairs. This includes the target URI for the request, the status code for the
 | |
| //   response, as well as HTTP header fields.
 | |
| IncomingResponse.prototype._onHeaders = function _onHeaders(headers) {
 | |
|   // * A single ":status" header field is defined that carries the HTTP status code field. This
 | |
|   //   header field MUST be included in all responses.
 | |
|   // * A client MUST treat the absence of the ":status" header field, the presence of multiple
 | |
|   //   values, or an invalid value as a stream error of type PROTOCOL_ERROR.
 | |
|   //   Note: currently, we do not enforce it strictly: we accept any format, and parse it as int
 | |
|   // * HTTP/2.0 does not define a way to carry the reason phrase that is included in an HTTP/1.1
 | |
|   //   status line.
 | |
|   this.statusCode = parseInt(this._checkSpecialHeader(':status', headers[':status']));
 | |
| 
 | |
|   // * Handling regular headers.
 | |
|   IncomingMessage.prototype._onHeaders.call(this, headers);
 | |
| 
 | |
|   // * Signaling that the headers arrived.
 | |
|   this._log.info({ status: this.statusCode, headers: this.headers}, 'Incoming response');
 | |
|   this.emit('ready');
 | |
| };
 | |
| 
 | |
| // IncomingPromise class
 | |
| // -------------------------
 | |
| 
 | |
| function IncomingPromise(responseStream, promiseHeaders) {
 | |
|   var stream = new Readable();
 | |
|   stream._read = noop;
 | |
|   stream.push(null);
 | |
|   stream._log = responseStream._log;
 | |
| 
 | |
|   IncomingRequest.call(this, stream);
 | |
| 
 | |
|   this._onHeaders(promiseHeaders);
 | |
| 
 | |
|   this._responseStream = responseStream;
 | |
| 
 | |
|   var response = new IncomingResponse(this._responseStream);
 | |
|   response.once('ready', this.emit.bind(this, 'response', response));
 | |
| 
 | |
|   this.stream.on('promise', this._onPromise.bind(this));
 | |
| }
 | |
| IncomingPromise.prototype = Object.create(IncomingRequest.prototype, { constructor: { value: IncomingPromise } });
 | |
| 
 | |
| IncomingPromise.prototype.cancel = function cancel() {
 | |
|   this._responseStream.reset('CANCEL');
 | |
| };
 | |
| 
 | |
| IncomingPromise.prototype.setPriority = function setPriority(priority) {
 | |
|   this._responseStream.priority(priority);
 | |
| };
 | |
| 
 | |
| IncomingPromise.prototype._onPromise = OutgoingRequest.prototype._onPromise;
 |