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

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

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

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

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

mercurial