Thu, 22 Jan 2015 13:21:57 +0100
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;