|
1 // Public API |
|
2 // ========== |
|
3 |
|
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 |
|
127 |
|
128 // Common server and client side code |
|
129 // ================================== |
|
130 |
|
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'); |
|
142 |
|
143 exports.STATUS_CODES = http.STATUS_CODES; |
|
144 exports.IncomingMessage = IncomingMessage; |
|
145 exports.OutgoingMessage = OutgoingMessage; |
|
146 |
|
147 var deprecatedHeaders = [ |
|
148 'connection', |
|
149 'host', |
|
150 'keep-alive', |
|
151 'proxy-connection', |
|
152 'te', |
|
153 'transfer-encoding', |
|
154 'upgrade' |
|
155 ]; |
|
156 |
|
157 // When doing NPN/ALPN negotiation, HTTP/1.1 is used as fallback |
|
158 var supportedProtocols = [implementedVersion, 'http/1.1', 'http/1.0']; |
|
159 |
|
160 // Logging |
|
161 // ------- |
|
162 |
|
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, |
|
172 |
|
173 child: function() { return this; } |
|
174 }; |
|
175 |
|
176 // Bunyan serializers exported by submodules that are worth adding when creating a logger. |
|
177 exports.serializers = require('http2-protocol').serializers; |
|
178 |
|
179 // IncomingMessage class |
|
180 // --------------------- |
|
181 |
|
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; |
|
187 |
|
188 this._log = stream._log.child({ component: 'http' }); |
|
189 |
|
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; |
|
195 |
|
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; |
|
200 |
|
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 } }); |
|
206 |
|
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 } |
|
224 |
|
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 } |
|
231 |
|
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 }; |
|
238 |
|
239 IncomingMessage.prototype._onEnd = function _onEnd() { |
|
240 this.trailers = this._lastHeadersSeen; |
|
241 }; |
|
242 |
|
243 IncomingMessage.prototype.setTimeout = noop; |
|
244 |
|
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 } |
|
250 |
|
251 return value; |
|
252 } |
|
253 ; |
|
254 |
|
255 // OutgoingMessage class |
|
256 // --------------------- |
|
257 |
|
258 function OutgoingMessage() { |
|
259 // * This is basically a read-only wrapper for the [Stream](stream.html) class. |
|
260 Writable.call(this); |
|
261 |
|
262 this._headers = {}; |
|
263 this._trailers = undefined; |
|
264 this.headersSent = false; |
|
265 |
|
266 this.on('finish', this._finish); |
|
267 } |
|
268 OutgoingMessage.prototype = Object.create(Writable.prototype, { constructor: { value: OutgoingMessage } }); |
|
269 |
|
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 }; |
|
277 |
|
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 }; |
|
292 |
|
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 }; |
|
304 |
|
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 }; |
|
312 |
|
313 OutgoingMessage.prototype.getHeader = function getHeader(name) { |
|
314 return this._headers[name.toLowerCase()]; |
|
315 }; |
|
316 |
|
317 OutgoingMessage.prototype.addTrailers = function addTrailers(trailers) { |
|
318 this._trailers = trailers; |
|
319 }; |
|
320 |
|
321 OutgoingMessage.prototype.setTimeout = noop; |
|
322 |
|
323 OutgoingMessage.prototype._checkSpecialHeader = IncomingMessage.prototype._checkSpecialHeader; |
|
324 |
|
325 // Server side |
|
326 // =========== |
|
327 |
|
328 exports.createServer = createServer; |
|
329 exports.Server = Server; |
|
330 exports.IncomingRequest = IncomingRequest; |
|
331 exports.OutgoingResponse = OutgoingResponse; |
|
332 exports.ServerResponse = OutgoingResponse; // for API compatibility |
|
333 |
|
334 // Server class |
|
335 // ------------ |
|
336 |
|
337 function Server(options) { |
|
338 options = util._extend({}, options); |
|
339 |
|
340 this._log = (options.log || defaultLogger).child({ component: 'http' }); |
|
341 this._settings = options.settings; |
|
342 |
|
343 var start = this._start.bind(this); |
|
344 var fallback = this._fallback.bind(this); |
|
345 |
|
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 } |
|
365 |
|
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 } |
|
372 |
|
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 } |
|
378 |
|
379 this._server.on('close', this.emit.bind(this, 'close')); |
|
380 } |
|
381 Server.prototype = Object.create(EventEmitter.prototype, { constructor: { value: Server } }); |
|
382 |
|
383 // Starting HTTP/2 |
|
384 Server.prototype._start = function _start(socket) { |
|
385 var endpoint = new Endpoint(this._log, 'SERVER', this._settings); |
|
386 |
|
387 this._log.info({ e: endpoint, |
|
388 client: socket.remoteAddress + ':' + socket.remotePort, |
|
389 SNI: socket.servername |
|
390 }, 'New incoming HTTP/2 connection'); |
|
391 |
|
392 endpoint.pipe(socket).pipe(endpoint); |
|
393 |
|
394 var self = this; |
|
395 endpoint.on('stream', function _onStream(stream) { |
|
396 var response = new OutgoingResponse(stream); |
|
397 var request = new IncomingRequest(stream); |
|
398 |
|
399 request.once('ready', self.emit.bind(self, 'request', request, response)); |
|
400 }); |
|
401 |
|
402 endpoint.on('error', this.emit.bind(this, 'clientError')); |
|
403 socket.on('error', this.emit.bind(this, 'clientError')); |
|
404 |
|
405 this.emit('connection', socket, endpoint); |
|
406 }; |
|
407 |
|
408 Server.prototype._fallback = function _fallback(socket) { |
|
409 var negotiatedProtocol = socket.alpnProtocol || socket.npnProtocol; |
|
410 |
|
411 this._log.info({ client: socket.remoteAddress + ':' + socket.remotePort, |
|
412 protocol: negotiatedProtocol, |
|
413 SNI: socket.servername |
|
414 }, 'Falling back to simple HTTPS'); |
|
415 |
|
416 for (var i = 0; i < this._originalSocketListeners.length; i++) { |
|
417 this._originalSocketListeners[i].call(this._server, socket); |
|
418 } |
|
419 |
|
420 this.emit('connection', socket); |
|
421 }; |
|
422 |
|
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 }; |
|
431 |
|
432 Server.prototype.close = function close(callback) { |
|
433 this._log.info('Closing server'); |
|
434 this._server.close(callback); |
|
435 }; |
|
436 |
|
437 Server.prototype.setTimeout = function setTimeout(timeout, callback) { |
|
438 if (this._mode === 'tls') { |
|
439 this._server.setTimeout(timeout, callback); |
|
440 } |
|
441 }; |
|
442 |
|
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 }); |
|
457 |
|
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 }; |
|
469 |
|
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 }; |
|
476 |
|
477 function createServer(options, requestListener) { |
|
478 if (typeof options === 'function') { |
|
479 requestListener = options; |
|
480 options = undefined; |
|
481 } |
|
482 |
|
483 var server = new Server(options); |
|
484 |
|
485 if (requestListener) { |
|
486 server.on('request', requestListener); |
|
487 } |
|
488 |
|
489 return server; |
|
490 } |
|
491 |
|
492 // IncomingRequest class |
|
493 // --------------------- |
|
494 |
|
495 function IncomingRequest(stream) { |
|
496 IncomingMessage.call(this, stream); |
|
497 } |
|
498 IncomingRequest.prototype = Object.create(IncomingMessage.prototype, { constructor: { value: IncomingRequest } }); |
|
499 |
|
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'] ); |
|
519 |
|
520 // * Host header is included in the headers object for backwards compatibility. |
|
521 this.headers.host = this.host; |
|
522 |
|
523 // * Handling regular headers. |
|
524 IncomingMessage.prototype._onHeaders.call(this, headers); |
|
525 |
|
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 }; |
|
531 |
|
532 // OutgoingResponse class |
|
533 // ---------------------- |
|
534 |
|
535 function OutgoingResponse(stream) { |
|
536 OutgoingMessage.call(this); |
|
537 |
|
538 this._log = stream._log.child({ component: 'http' }); |
|
539 |
|
540 this.stream = stream; |
|
541 this.statusCode = 200; |
|
542 this.sendDate = true; |
|
543 |
|
544 this.stream.once('headers', this._onRequestHeaders.bind(this)); |
|
545 } |
|
546 OutgoingResponse.prototype = Object.create(OutgoingMessage.prototype, { constructor: { value: OutgoingResponse } }); |
|
547 |
|
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 } |
|
554 |
|
555 for (var name in headers) { |
|
556 this.setHeader(name, headers[name]); |
|
557 } |
|
558 headers = this._headers; |
|
559 |
|
560 if (this.sendDate && !('date' in this._headers)) { |
|
561 headers.date = (new Date()).toUTCString(); |
|
562 } |
|
563 |
|
564 this._log.info({ status: statusCode, headers: this._headers }, 'Sending server response'); |
|
565 |
|
566 headers[':status'] = this.statusCode = statusCode; |
|
567 |
|
568 this.stream.headers(headers); |
|
569 this.headersSent = true; |
|
570 }; |
|
571 |
|
572 OutgoingResponse.prototype._implicitHeaders = function _implicitHeaders() { |
|
573 if (!this.headersSent) { |
|
574 this.writeHead(this.statusCode); |
|
575 } |
|
576 }; |
|
577 |
|
578 OutgoingResponse.prototype.write = function write() { |
|
579 this._implicitHeaders(); |
|
580 return OutgoingMessage.prototype.write.apply(this, arguments); |
|
581 }; |
|
582 |
|
583 OutgoingResponse.prototype.end = function end() { |
|
584 this._implicitHeaders(); |
|
585 return OutgoingMessage.prototype.end.apply(this, arguments); |
|
586 }; |
|
587 |
|
588 OutgoingResponse.prototype._onRequestHeaders = function _onRequestHeaders(headers) { |
|
589 this._requestHeaders = headers; |
|
590 }; |
|
591 |
|
592 OutgoingResponse.prototype.push = function push(options) { |
|
593 if (typeof options === 'string') { |
|
594 options = url.parse(options); |
|
595 } |
|
596 |
|
597 if (!options.path) { |
|
598 throw new Error('`path` option is mandatory.'); |
|
599 } |
|
600 |
|
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); |
|
607 |
|
608 this._log.info({ method: promise[':method'], scheme: promise[':scheme'], |
|
609 authority: promise[':authority'], path: promise[':path'], |
|
610 headers: options.headers }, 'Promising push stream'); |
|
611 |
|
612 var pushStream = this.stream.promise(promise); |
|
613 |
|
614 return new OutgoingResponse(pushStream); |
|
615 }; |
|
616 |
|
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 }; |
|
626 |
|
627 // Client side |
|
628 // =========== |
|
629 |
|
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 }; |
|
641 |
|
642 // Agent class |
|
643 // ----------- |
|
644 |
|
645 function Agent(options) { |
|
646 EventEmitter.call(this); |
|
647 |
|
648 options = util._extend({}, options); |
|
649 |
|
650 this._settings = options.settings; |
|
651 this._log = (options.log || defaultLogger).child({ component: 'http' }); |
|
652 this.endpoints = {}; |
|
653 |
|
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); |
|
662 |
|
663 this.sockets = this._httpsAgent.sockets; |
|
664 this.requests = this._httpsAgent.requests; |
|
665 } |
|
666 Agent.prototype = Object.create(EventEmitter.prototype, { constructor: { value: Agent } }); |
|
667 |
|
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 } |
|
674 |
|
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 || '/'; |
|
680 |
|
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 } |
|
685 |
|
686 var request = new OutgoingRequest(this._log); |
|
687 |
|
688 if (callback) { |
|
689 request.on('response', callback); |
|
690 } |
|
691 |
|
692 var key = [ |
|
693 !!options.plain, |
|
694 options.host, |
|
695 options.port |
|
696 ].join(':'); |
|
697 |
|
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 } |
|
703 |
|
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 } |
|
715 |
|
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); |
|
724 |
|
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 }); |
|
733 |
|
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 } |
|
762 |
|
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 } |
|
772 |
|
773 return request; |
|
774 }; |
|
775 |
|
776 Agent.prototype.get = function get(options, callback) { |
|
777 var request = this.request(options, callback); |
|
778 request.end(); |
|
779 return request; |
|
780 }; |
|
781 |
|
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 } |
|
792 |
|
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 }); |
|
801 |
|
802 exports.globalAgent = new Agent(); |
|
803 |
|
804 // OutgoingRequest class |
|
805 // --------------------- |
|
806 |
|
807 function OutgoingRequest() { |
|
808 OutgoingMessage.call(this); |
|
809 |
|
810 this._log = undefined; |
|
811 |
|
812 this.stream = undefined; |
|
813 } |
|
814 OutgoingRequest.prototype = Object.create(OutgoingMessage.prototype, { constructor: { value: OutgoingRequest } }); |
|
815 |
|
816 OutgoingRequest.prototype._start = function _start(stream, options) { |
|
817 this.stream = stream; |
|
818 |
|
819 this._log = stream._log.child({ component: 'http' }); |
|
820 |
|
821 for (var key in options.headers) { |
|
822 this.setHeader(key, options.headers[key]); |
|
823 } |
|
824 var headers = this._headers; |
|
825 delete headers.host; |
|
826 |
|
827 if (options.auth) { |
|
828 headers.authorization = 'Basic ' + new Buffer(options.auth).toString('base64'); |
|
829 } |
|
830 |
|
831 headers[':scheme'] = options.protocol.slice(0, -1); |
|
832 headers[':method'] = options.method; |
|
833 headers[':authority'] = options.host; |
|
834 headers[':path'] = options.path; |
|
835 |
|
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; |
|
841 |
|
842 this.emit('socket', this.stream); |
|
843 |
|
844 var response = new IncomingResponse(this.stream); |
|
845 response.once('ready', this.emit.bind(this, 'response', response)); |
|
846 |
|
847 this.stream.on('promise', this._onPromise.bind(this)); |
|
848 }; |
|
849 |
|
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 }; |
|
855 |
|
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 }; |
|
863 |
|
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 }; |
|
873 |
|
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 }; |
|
882 |
|
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 }; |
|
890 |
|
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 }; |
|
898 |
|
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 }; |
|
909 |
|
910 // Receiving push promises |
|
911 OutgoingRequest.prototype._onPromise = function _onPromise(stream, headers) { |
|
912 this._log.info({ push_stream: stream.id }, 'Receiving push promise'); |
|
913 |
|
914 var promise = new IncomingPromise(stream, headers); |
|
915 |
|
916 if (this.listeners('push').length > 0) { |
|
917 this.emit('push', promise); |
|
918 } else { |
|
919 promise.cancel(); |
|
920 } |
|
921 }; |
|
922 |
|
923 // IncomingResponse class |
|
924 // ---------------------- |
|
925 |
|
926 function IncomingResponse(stream) { |
|
927 IncomingMessage.call(this, stream); |
|
928 } |
|
929 IncomingResponse.prototype = Object.create(IncomingMessage.prototype, { constructor: { value: IncomingResponse } }); |
|
930 |
|
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'])); |
|
944 |
|
945 // * Handling regular headers. |
|
946 IncomingMessage.prototype._onHeaders.call(this, headers); |
|
947 |
|
948 // * Signaling that the headers arrived. |
|
949 this._log.info({ status: this.statusCode, headers: this.headers}, 'Incoming response'); |
|
950 this.emit('ready'); |
|
951 }; |
|
952 |
|
953 // IncomingPromise class |
|
954 // ------------------------- |
|
955 |
|
956 function IncomingPromise(responseStream, promiseHeaders) { |
|
957 var stream = new Readable(); |
|
958 stream._read = noop; |
|
959 stream.push(null); |
|
960 stream._log = responseStream._log; |
|
961 |
|
962 IncomingRequest.call(this, stream); |
|
963 |
|
964 this._onHeaders(promiseHeaders); |
|
965 |
|
966 this._responseStream = responseStream; |
|
967 |
|
968 var response = new IncomingResponse(this._responseStream); |
|
969 response.once('ready', this.emit.bind(this, 'response', response)); |
|
970 |
|
971 this.stream.on('promise', this._onPromise.bind(this)); |
|
972 } |
|
973 IncomingPromise.prototype = Object.create(IncomingRequest.prototype, { constructor: { value: IncomingPromise } }); |
|
974 |
|
975 IncomingPromise.prototype.cancel = function cancel() { |
|
976 this._responseStream.reset('CANCEL'); |
|
977 }; |
|
978 |
|
979 IncomingPromise.prototype.setPriority = function setPriority(priority) { |
|
980 this._responseStream.priority(priority); |
|
981 }; |
|
982 |
|
983 IncomingPromise.prototype._onPromise = OutgoingRequest.prototype._onPromise; |