1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/testing/xpcshell/node-http2/lib/http.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,983 @@ 1.4 +// Public API 1.5 +// ========== 1.6 + 1.7 +// The main governing power behind the http2 API design is that it should look very similar to the 1.8 +// existing node.js [HTTPS API][1] (which is, in turn, almost identical to the [HTTP API][2]). The 1.9 +// additional features of HTTP/2 are exposed as extensions to this API. Furthermore, node-http2 1.10 +// should fall back to using HTTP/1.1 if needed. Compatibility with undocumented or deprecated 1.11 +// elements of the node.js HTTP/HTTPS API is a non-goal. 1.12 +// 1.13 +// Additional and modified API elements 1.14 +// ------------------------------------ 1.15 +// 1.16 +// - **Class: http2.Endpoint**: an API for using the raw HTTP/2 framing layer. For documentation 1.17 +// see the [lib/endpoint.js](endpoint.html) file. 1.18 +// 1.19 +// - **Class: http2.Server** 1.20 +// - **Event: 'connection' (socket, [endpoint])**: there's a second argument if the negotiation of 1.21 +// HTTP/2 was successful: the reference to the [Endpoint](endpoint.html) object tied to the 1.22 +// socket. 1.23 +// 1.24 +// - **http2.createServer(options, [requestListener])**: additional option: 1.25 +// - **log**: an optional [bunyan](https://github.com/trentm/node-bunyan) logger object 1.26 +// - **plain**: if `true`, the server will accept HTTP/2 connections over plain TCP instead of 1.27 +// TLS 1.28 +// 1.29 +// - **Class: http2.ServerResponse** 1.30 +// - **response.push(options)**: initiates a server push. `options` describes the 'imaginary' 1.31 +// request to which the push stream is a response; the possible options are identical to the 1.32 +// ones accepted by `http2.request`. Returns a ServerResponse object that can be used to send 1.33 +// the response headers and content. 1.34 +// 1.35 +// - **Class: http2.Agent** 1.36 +// - **new Agent(options)**: additional option: 1.37 +// - **log**: an optional [bunyan](https://github.com/trentm/node-bunyan) logger object 1.38 +// - **agent.sockets**: only contains TCP sockets that corresponds to HTTP/1 requests. 1.39 +// - **agent.endpoints**: contains [Endpoint](endpoint.html) objects for HTTP/2 connections. 1.40 +// 1.41 +// - **http2.request(options, [callback])**: additional option: 1.42 +// - **plain**: if `true`, the client will not try to build a TLS tunnel, instead it will use 1.43 +// the raw TCP stream for HTTP/2 1.44 +// 1.45 +// - **Class: http2.ClientRequest** 1.46 +// - **Event: 'socket' (socket)**: in case of an HTTP/2 incoming message, `socket` is a reference 1.47 +// to the associated [HTTP/2 Stream](stream.html) object (and not to the TCP socket). 1.48 +// - **Event: 'push' (promise)**: signals the intention of a server push associated to this 1.49 +// request. `promise` is an IncomingPromise. If there's no listener for this event, the server 1.50 +// push is cancelled. 1.51 +// - **request.setPriority(priority)**: assign a priority to this request. `priority` is a number 1.52 +// between 0 (highest priority) and 2^31-1 (lowest priority). Default value is 2^30. 1.53 +// 1.54 +// - **Class: http2.IncomingMessage** 1.55 +// - has two subclasses for easier interface description: **IncomingRequest** and 1.56 +// **IncomingResponse** 1.57 +// - **message.socket**: in case of an HTTP/2 incoming message, it's a reference to the associated 1.58 +// [HTTP/2 Stream](stream.html) object (and not to the TCP socket). 1.59 +// 1.60 +// - **Class: http2.IncomingRequest (IncomingMessage)** 1.61 +// - **message.url**: in case of an HTTP/2 incoming request, the `url` field always contains the 1.62 +// path, and never a full url (it contains the path in most cases in the HTTPS api as well). 1.63 +// - **message.scheme**: additional field. Mandatory HTTP/2 request metadata. 1.64 +// - **message.host**: additional field. Mandatory HTTP/2 request metadata. Note that this 1.65 +// replaces the old Host header field, but node-http2 will add Host to the `message.headers` for 1.66 +// backwards compatibility. 1.67 +// 1.68 +// - **Class: http2.IncomingPromise (IncomingRequest)** 1.69 +// - contains the metadata of the 'imaginary' request to which the server push is an answer. 1.70 +// - **Event: 'response' (response)**: signals the arrival of the actual push stream. `response` 1.71 +// is an IncomingResponse. 1.72 +// - **Event: 'push' (promise)**: signals the intention of a server push associated to this 1.73 +// request. `promise` is an IncomingPromise. If there's no listener for this event, the server 1.74 +// push is cancelled. 1.75 +// - **promise.cancel()**: cancels the promised server push. 1.76 +// - **promise.setPriority(priority)**: assign a priority to this push stream. `priority` is a 1.77 +// number between 0 (highest priority) and 2^31-1 (lowest priority). Default value is 2^30. 1.78 +// 1.79 +// API elements not yet implemented 1.80 +// -------------------------------- 1.81 +// 1.82 +// - **Class: http2.Server** 1.83 +// - **server.maxHeadersCount** 1.84 +// 1.85 +// API elements that are not applicable to HTTP/2 1.86 +// ---------------------------------------------- 1.87 +// 1.88 +// The reason may be deprecation of certain HTTP/1.1 features, or that some API elements simply 1.89 +// don't make sense when using HTTP/2. These will not be present when a request is done with HTTP/2, 1.90 +// but will function normally when falling back to using HTTP/1.1. 1.91 +// 1.92 +// - **Class: http2.Server** 1.93 +// - **Event: 'checkContinue'**: not in the spec, yet (see [http-spec#18][expect-continue]) 1.94 +// - **Event: 'upgrade'**: upgrade is deprecated in HTTP/2 1.95 +// - **Event: 'timeout'**: HTTP/2 sockets won't timeout because of application level keepalive 1.96 +// (PING frames) 1.97 +// - **Event: 'connect'**: not in the spec, yet (see [http-spec#230][connect]) 1.98 +// - **server.setTimeout(msecs, [callback])** 1.99 +// - **server.timeout** 1.100 +// 1.101 +// - **Class: http2.ServerResponse** 1.102 +// - **Event: 'close'** 1.103 +// - **Event: 'timeout'** 1.104 +// - **response.writeContinue()** 1.105 +// - **response.writeHead(statusCode, [reasonPhrase], [headers])**: reasonPhrase will always be 1.106 +// ignored since [it's not supported in HTTP/2][3] 1.107 +// - **response.setTimeout(timeout, [callback])** 1.108 +// 1.109 +// - **Class: http2.Agent** 1.110 +// - **agent.maxSockets**: only affects HTTP/1 connection pool. When using HTTP/2, there's always 1.111 +// one connection per host. 1.112 +// 1.113 +// - **Class: http2.ClientRequest** 1.114 +// - **Event: 'upgrade'** 1.115 +// - **Event: 'connect'** 1.116 +// - **Event: 'continue'** 1.117 +// - **request.setTimeout(timeout, [callback])** 1.118 +// - **request.setNoDelay([noDelay])** 1.119 +// - **request.setSocketKeepAlive([enable], [initialDelay])** 1.120 +// 1.121 +// - **Class: http2.IncomingMessage** 1.122 +// - **Event: 'close'** 1.123 +// - **message.setTimeout(timeout, [callback])** 1.124 +// 1.125 +// [1]: http://nodejs.org/api/https.html 1.126 +// [2]: http://nodejs.org/api/http.html 1.127 +// [3]: http://tools.ietf.org/html/draft-ietf-httpbis-http2-10#section-8.1.3.2 1.128 +// [expect-continue]: https://github.com/http2/http2-spec/issues/18 1.129 +// [connect]: https://github.com/http2/http2-spec/issues/230 1.130 + 1.131 +// Common server and client side code 1.132 +// ================================== 1.133 + 1.134 +var net = require('net'); 1.135 +var url = require('url'); 1.136 +var util = require('util'); 1.137 +var EventEmitter = require('events').EventEmitter; 1.138 +var PassThrough = require('stream').PassThrough; 1.139 +var Readable = require('stream').Readable; 1.140 +var Writable = require('stream').Writable; 1.141 +var Endpoint = require('http2-protocol').Endpoint; 1.142 +var implementedVersion = require('http2-protocol').ImplementedVersion; 1.143 +var http = require('http'); 1.144 +var https = require('https'); 1.145 + 1.146 +exports.STATUS_CODES = http.STATUS_CODES; 1.147 +exports.IncomingMessage = IncomingMessage; 1.148 +exports.OutgoingMessage = OutgoingMessage; 1.149 + 1.150 +var deprecatedHeaders = [ 1.151 + 'connection', 1.152 + 'host', 1.153 + 'keep-alive', 1.154 + 'proxy-connection', 1.155 + 'te', 1.156 + 'transfer-encoding', 1.157 + 'upgrade' 1.158 +]; 1.159 + 1.160 +// When doing NPN/ALPN negotiation, HTTP/1.1 is used as fallback 1.161 +var supportedProtocols = [implementedVersion, 'http/1.1', 'http/1.0']; 1.162 + 1.163 +// Logging 1.164 +// ------- 1.165 + 1.166 +// Logger shim, used when no logger is provided by the user. 1.167 +function noop() {} 1.168 +var defaultLogger = { 1.169 + fatal: noop, 1.170 + error: noop, 1.171 + warn : noop, 1.172 + info : noop, 1.173 + debug: noop, 1.174 + trace: noop, 1.175 + 1.176 + child: function() { return this; } 1.177 +}; 1.178 + 1.179 +// Bunyan serializers exported by submodules that are worth adding when creating a logger. 1.180 +exports.serializers = require('http2-protocol').serializers; 1.181 + 1.182 +// IncomingMessage class 1.183 +// --------------------- 1.184 + 1.185 +function IncomingMessage(stream) { 1.186 + // * This is basically a read-only wrapper for the [Stream](stream.html) class. 1.187 + PassThrough.call(this); 1.188 + stream.pipe(this); 1.189 + this.socket = this.stream = stream; 1.190 + 1.191 + this._log = stream._log.child({ component: 'http' }); 1.192 + 1.193 + // * HTTP/2.0 does not define a way to carry the version identifier that is included in the 1.194 + // HTTP/1.1 request/status line. Version is always 2.0. 1.195 + this.httpVersion = '2.0'; 1.196 + this.httpVersionMajor = 2; 1.197 + this.httpVersionMinor = 0; 1.198 + 1.199 + // * `this.headers` will store the regular headers (and none of the special colon headers) 1.200 + this.headers = {}; 1.201 + this.trailers = undefined; 1.202 + this._lastHeadersSeen = undefined; 1.203 + 1.204 + // * Other metadata is filled in when the headers arrive. 1.205 + stream.once('headers', this._onHeaders.bind(this)); 1.206 + stream.once('end', this._onEnd.bind(this)); 1.207 +} 1.208 +IncomingMessage.prototype = Object.create(PassThrough.prototype, { constructor: { value: IncomingMessage } }); 1.209 + 1.210 +// [Request Header Fields](http://tools.ietf.org/html/draft-ietf-httpbis-http2-10#section-8.1.3.1) 1.211 +// * `headers` argument: HTTP/2.0 request and response header fields carry information as a series 1.212 +// of key-value pairs. This includes the target URI for the request, the status code for the 1.213 +// response, as well as HTTP header fields. 1.214 +IncomingMessage.prototype._onHeaders = function _onHeaders(headers) { 1.215 + // * An HTTP/2.0 request or response MUST NOT include any of the following header fields: 1.216 + // Connection, Host, Keep-Alive, Proxy-Connection, TE, Transfer-Encoding, and Upgrade. A server 1.217 + // MUST treat the presence of any of these header fields as a stream error of type 1.218 + // PROTOCOL_ERROR. 1.219 + for (var i = 0; i < deprecatedHeaders.length; i++) { 1.220 + var key = deprecatedHeaders[i]; 1.221 + if (key in headers) { 1.222 + this._log.error({ key: key, value: headers[key] }, 'Deprecated header found'); 1.223 + this.stream.emit('error', 'PROTOCOL_ERROR'); 1.224 + return; 1.225 + } 1.226 + } 1.227 + 1.228 + // * Store the _regular_ headers in `this.headers` 1.229 + for (var name in headers) { 1.230 + if (name[0] !== ':') { 1.231 + this.headers[name] = headers[name]; 1.232 + } 1.233 + } 1.234 + 1.235 + // * The last header block, if it's not the first, will represent the trailers 1.236 + var self = this; 1.237 + this.stream.on('headers', function(headers) { 1.238 + self._lastHeadersSeen = headers; 1.239 + }); 1.240 +}; 1.241 + 1.242 +IncomingMessage.prototype._onEnd = function _onEnd() { 1.243 + this.trailers = this._lastHeadersSeen; 1.244 +}; 1.245 + 1.246 +IncomingMessage.prototype.setTimeout = noop; 1.247 + 1.248 +IncomingMessage.prototype._checkSpecialHeader = function _checkSpecialHeader(key, value) { 1.249 + if ((typeof value !== 'string') || (value.length === 0)) { 1.250 + this._log.error({ key: key, value: value }, 'Invalid or missing special header field'); 1.251 + this.stream.emit('error', 'PROTOCOL_ERROR'); 1.252 + } 1.253 + 1.254 + return value; 1.255 +} 1.256 +; 1.257 + 1.258 +// OutgoingMessage class 1.259 +// --------------------- 1.260 + 1.261 +function OutgoingMessage() { 1.262 + // * This is basically a read-only wrapper for the [Stream](stream.html) class. 1.263 + Writable.call(this); 1.264 + 1.265 + this._headers = {}; 1.266 + this._trailers = undefined; 1.267 + this.headersSent = false; 1.268 + 1.269 + this.on('finish', this._finish); 1.270 +} 1.271 +OutgoingMessage.prototype = Object.create(Writable.prototype, { constructor: { value: OutgoingMessage } }); 1.272 + 1.273 +OutgoingMessage.prototype._write = function _write(chunk, encoding, callback) { 1.274 + if (this.stream) { 1.275 + this.stream.write(chunk, encoding, callback); 1.276 + } else { 1.277 + this.once('socket', this._write.bind(this, chunk, encoding, callback)); 1.278 + } 1.279 +}; 1.280 + 1.281 +OutgoingMessage.prototype._finish = function _finish() { 1.282 + if (this.stream) { 1.283 + if (this._trailers) { 1.284 + if (this.request) { 1.285 + this.request.addTrailers(this._trailers); 1.286 + } else { 1.287 + this.stream.headers(this._trailers); 1.288 + } 1.289 + } 1.290 + this.stream.end(); 1.291 + } else { 1.292 + this.once('socket', this._finish.bind(this)); 1.293 + } 1.294 +}; 1.295 + 1.296 +OutgoingMessage.prototype.setHeader = function setHeader(name, value) { 1.297 + if (this.headersSent) { 1.298 + throw new Error('Can\'t set headers after they are sent.'); 1.299 + } else { 1.300 + name = name.toLowerCase(); 1.301 + if (deprecatedHeaders.indexOf(name) !== -1) { 1.302 + throw new Error('Cannot set deprecated header: ' + name); 1.303 + } 1.304 + this._headers[name] = value; 1.305 + } 1.306 +}; 1.307 + 1.308 +OutgoingMessage.prototype.removeHeader = function removeHeader(name) { 1.309 + if (this.headersSent) { 1.310 + throw new Error('Can\'t remove headers after they are sent.'); 1.311 + } else { 1.312 + delete this._headers[name.toLowerCase()]; 1.313 + } 1.314 +}; 1.315 + 1.316 +OutgoingMessage.prototype.getHeader = function getHeader(name) { 1.317 + return this._headers[name.toLowerCase()]; 1.318 +}; 1.319 + 1.320 +OutgoingMessage.prototype.addTrailers = function addTrailers(trailers) { 1.321 + this._trailers = trailers; 1.322 +}; 1.323 + 1.324 +OutgoingMessage.prototype.setTimeout = noop; 1.325 + 1.326 +OutgoingMessage.prototype._checkSpecialHeader = IncomingMessage.prototype._checkSpecialHeader; 1.327 + 1.328 +// Server side 1.329 +// =========== 1.330 + 1.331 +exports.createServer = createServer; 1.332 +exports.Server = Server; 1.333 +exports.IncomingRequest = IncomingRequest; 1.334 +exports.OutgoingResponse = OutgoingResponse; 1.335 +exports.ServerResponse = OutgoingResponse; // for API compatibility 1.336 + 1.337 +// Server class 1.338 +// ------------ 1.339 + 1.340 +function Server(options) { 1.341 + options = util._extend({}, options); 1.342 + 1.343 + this._log = (options.log || defaultLogger).child({ component: 'http' }); 1.344 + this._settings = options.settings; 1.345 + 1.346 + var start = this._start.bind(this); 1.347 + var fallback = this._fallback.bind(this); 1.348 + 1.349 + // HTTP2 over TLS (using NPN or ALPN) 1.350 + if ((options.key && options.cert) || options.pfx) { 1.351 + this._log.info('Creating HTTP/2 server over TLS'); 1.352 + this._mode = 'tls'; 1.353 + options.ALPNProtocols = supportedProtocols; 1.354 + options.NPNProtocols = supportedProtocols; 1.355 + this._server = https.createServer(options); 1.356 + this._originalSocketListeners = this._server.listeners('secureConnection'); 1.357 + this._server.removeAllListeners('secureConnection'); 1.358 + this._server.on('secureConnection', function(socket) { 1.359 + var negotiatedProtocol = socket.alpnProtocol || socket.npnProtocol; 1.360 + if ((negotiatedProtocol === implementedVersion) && socket.servername) { 1.361 + start(socket); 1.362 + } else { 1.363 + fallback(socket); 1.364 + } 1.365 + }); 1.366 + this._server.on('request', this.emit.bind(this, 'request')); 1.367 + } 1.368 + 1.369 + // HTTP2 over plain TCP 1.370 + else if (options.plain) { 1.371 + this._log.info('Creating HTTP/2 server over plain TCP'); 1.372 + this._mode = 'plain'; 1.373 + this._server = net.createServer(start); 1.374 + } 1.375 + 1.376 + // HTTP/2 with HTTP/1.1 upgrade 1.377 + else { 1.378 + this._log.error('Trying to create HTTP/2 server with Upgrade from HTTP/1.1'); 1.379 + throw new Error('HTTP1.1 -> HTTP2 upgrade is not yet supported. Please provide TLS keys.'); 1.380 + } 1.381 + 1.382 + this._server.on('close', this.emit.bind(this, 'close')); 1.383 +} 1.384 +Server.prototype = Object.create(EventEmitter.prototype, { constructor: { value: Server } }); 1.385 + 1.386 +// Starting HTTP/2 1.387 +Server.prototype._start = function _start(socket) { 1.388 + var endpoint = new Endpoint(this._log, 'SERVER', this._settings); 1.389 + 1.390 + this._log.info({ e: endpoint, 1.391 + client: socket.remoteAddress + ':' + socket.remotePort, 1.392 + SNI: socket.servername 1.393 + }, 'New incoming HTTP/2 connection'); 1.394 + 1.395 + endpoint.pipe(socket).pipe(endpoint); 1.396 + 1.397 + var self = this; 1.398 + endpoint.on('stream', function _onStream(stream) { 1.399 + var response = new OutgoingResponse(stream); 1.400 + var request = new IncomingRequest(stream); 1.401 + 1.402 + request.once('ready', self.emit.bind(self, 'request', request, response)); 1.403 + }); 1.404 + 1.405 + endpoint.on('error', this.emit.bind(this, 'clientError')); 1.406 + socket.on('error', this.emit.bind(this, 'clientError')); 1.407 + 1.408 + this.emit('connection', socket, endpoint); 1.409 +}; 1.410 + 1.411 +Server.prototype._fallback = function _fallback(socket) { 1.412 + var negotiatedProtocol = socket.alpnProtocol || socket.npnProtocol; 1.413 + 1.414 + this._log.info({ client: socket.remoteAddress + ':' + socket.remotePort, 1.415 + protocol: negotiatedProtocol, 1.416 + SNI: socket.servername 1.417 + }, 'Falling back to simple HTTPS'); 1.418 + 1.419 + for (var i = 0; i < this._originalSocketListeners.length; i++) { 1.420 + this._originalSocketListeners[i].call(this._server, socket); 1.421 + } 1.422 + 1.423 + this.emit('connection', socket); 1.424 +}; 1.425 + 1.426 +// There are [3 possible signatures][1] of the `listen` function. Every arguments is forwarded to 1.427 +// the backing TCP or HTTPS server. 1.428 +// [1]: http://nodejs.org/api/http.html#http_server_listen_port_hostname_backlog_callback 1.429 +Server.prototype.listen = function listen(port, hostname) { 1.430 + this._log.info({ on: ((typeof hostname === 'string') ? (hostname + ':' + port) : port) }, 1.431 + 'Listening for incoming connections'); 1.432 + this._server.listen.apply(this._server, arguments); 1.433 +}; 1.434 + 1.435 +Server.prototype.close = function close(callback) { 1.436 + this._log.info('Closing server'); 1.437 + this._server.close(callback); 1.438 +}; 1.439 + 1.440 +Server.prototype.setTimeout = function setTimeout(timeout, callback) { 1.441 + if (this._mode === 'tls') { 1.442 + this._server.setTimeout(timeout, callback); 1.443 + } 1.444 +}; 1.445 + 1.446 +Object.defineProperty(Server.prototype, 'timeout', { 1.447 + get: function getTimeout() { 1.448 + if (this._mode === 'tls') { 1.449 + return this._server.timeout; 1.450 + } else { 1.451 + return undefined; 1.452 + } 1.453 + }, 1.454 + set: function setTimeout(timeout) { 1.455 + if (this._mode === 'tls') { 1.456 + this._server.timeout = timeout; 1.457 + } 1.458 + } 1.459 +}); 1.460 + 1.461 +// Overriding `EventEmitter`'s `on(event, listener)` method to forward certain subscriptions to 1.462 +// `server`.There are events on the `http.Server` class where it makes difference whether someone is 1.463 +// listening on the event or not. In these cases, we can not simply forward the events from the 1.464 +// `server` to `this` since that means a listener. Instead, we forward the subscriptions. 1.465 +Server.prototype.on = function on(event, listener) { 1.466 + if ((event === 'upgrade') || (event === 'timeout')) { 1.467 + this._server.on(event, listener && listener.bind(this)); 1.468 + } else { 1.469 + EventEmitter.prototype.on.call(this, event, listener); 1.470 + } 1.471 +}; 1.472 + 1.473 +// `addContext` is used to add Server Name Indication contexts 1.474 +Server.prototype.addContext = function addContext(hostname, credentials) { 1.475 + if (this._mode === 'tls') { 1.476 + this._server.addContext(hostname, credentials); 1.477 + } 1.478 +}; 1.479 + 1.480 +function createServer(options, requestListener) { 1.481 + if (typeof options === 'function') { 1.482 + requestListener = options; 1.483 + options = undefined; 1.484 + } 1.485 + 1.486 + var server = new Server(options); 1.487 + 1.488 + if (requestListener) { 1.489 + server.on('request', requestListener); 1.490 + } 1.491 + 1.492 + return server; 1.493 +} 1.494 + 1.495 +// IncomingRequest class 1.496 +// --------------------- 1.497 + 1.498 +function IncomingRequest(stream) { 1.499 + IncomingMessage.call(this, stream); 1.500 +} 1.501 +IncomingRequest.prototype = Object.create(IncomingMessage.prototype, { constructor: { value: IncomingRequest } }); 1.502 + 1.503 +// [Request Header Fields](http://tools.ietf.org/html/draft-ietf-httpbis-http2-10#section-8.1.3.1) 1.504 +// * `headers` argument: HTTP/2.0 request and response header fields carry information as a series 1.505 +// of key-value pairs. This includes the target URI for the request, the status code for the 1.506 +// response, as well as HTTP header fields. 1.507 +IncomingRequest.prototype._onHeaders = function _onHeaders(headers) { 1.508 + // * The ":method" header field includes the HTTP method 1.509 + // * The ":scheme" header field includes the scheme portion of the target URI 1.510 + // * The ":authority" header field includes the authority portion of the target URI 1.511 + // * The ":path" header field includes the path and query parts of the target URI. 1.512 + // This field MUST NOT be empty; URIs that do not contain a path component MUST include a value 1.513 + // of '/', unless the request is an OPTIONS request for '*', in which case the ":path" header 1.514 + // field MUST include '*'. 1.515 + // * All HTTP/2.0 requests MUST include exactly one valid value for all of these header fields. A 1.516 + // server MUST treat the absence of any of these header fields, presence of multiple values, or 1.517 + // an invalid value as a stream error of type PROTOCOL_ERROR. 1.518 + this.method = this._checkSpecialHeader(':method' , headers[':method']); 1.519 + this.scheme = this._checkSpecialHeader(':scheme' , headers[':scheme']); 1.520 + this.host = this._checkSpecialHeader(':authority', headers[':authority'] ); 1.521 + this.url = this._checkSpecialHeader(':path' , headers[':path'] ); 1.522 + 1.523 + // * Host header is included in the headers object for backwards compatibility. 1.524 + this.headers.host = this.host; 1.525 + 1.526 + // * Handling regular headers. 1.527 + IncomingMessage.prototype._onHeaders.call(this, headers); 1.528 + 1.529 + // * Signaling that the headers arrived. 1.530 + this._log.info({ method: this.method, scheme: this.scheme, host: this.host, 1.531 + path: this.url, headers: this.headers }, 'Incoming request'); 1.532 + this.emit('ready'); 1.533 +}; 1.534 + 1.535 +// OutgoingResponse class 1.536 +// ---------------------- 1.537 + 1.538 +function OutgoingResponse(stream) { 1.539 + OutgoingMessage.call(this); 1.540 + 1.541 + this._log = stream._log.child({ component: 'http' }); 1.542 + 1.543 + this.stream = stream; 1.544 + this.statusCode = 200; 1.545 + this.sendDate = true; 1.546 + 1.547 + this.stream.once('headers', this._onRequestHeaders.bind(this)); 1.548 +} 1.549 +OutgoingResponse.prototype = Object.create(OutgoingMessage.prototype, { constructor: { value: OutgoingResponse } }); 1.550 + 1.551 +OutgoingResponse.prototype.writeHead = function writeHead(statusCode, reasonPhrase, headers) { 1.552 + if (typeof reasonPhrase === 'string') { 1.553 + this._log.warn('Reason phrase argument was present but ignored by the writeHead method'); 1.554 + } else { 1.555 + headers = reasonPhrase; 1.556 + } 1.557 + 1.558 + for (var name in headers) { 1.559 + this.setHeader(name, headers[name]); 1.560 + } 1.561 + headers = this._headers; 1.562 + 1.563 + if (this.sendDate && !('date' in this._headers)) { 1.564 + headers.date = (new Date()).toUTCString(); 1.565 + } 1.566 + 1.567 + this._log.info({ status: statusCode, headers: this._headers }, 'Sending server response'); 1.568 + 1.569 + headers[':status'] = this.statusCode = statusCode; 1.570 + 1.571 + this.stream.headers(headers); 1.572 + this.headersSent = true; 1.573 +}; 1.574 + 1.575 +OutgoingResponse.prototype._implicitHeaders = function _implicitHeaders() { 1.576 + if (!this.headersSent) { 1.577 + this.writeHead(this.statusCode); 1.578 + } 1.579 +}; 1.580 + 1.581 +OutgoingResponse.prototype.write = function write() { 1.582 + this._implicitHeaders(); 1.583 + return OutgoingMessage.prototype.write.apply(this, arguments); 1.584 +}; 1.585 + 1.586 +OutgoingResponse.prototype.end = function end() { 1.587 + this._implicitHeaders(); 1.588 + return OutgoingMessage.prototype.end.apply(this, arguments); 1.589 +}; 1.590 + 1.591 +OutgoingResponse.prototype._onRequestHeaders = function _onRequestHeaders(headers) { 1.592 + this._requestHeaders = headers; 1.593 +}; 1.594 + 1.595 +OutgoingResponse.prototype.push = function push(options) { 1.596 + if (typeof options === 'string') { 1.597 + options = url.parse(options); 1.598 + } 1.599 + 1.600 + if (!options.path) { 1.601 + throw new Error('`path` option is mandatory.'); 1.602 + } 1.603 + 1.604 + var promise = util._extend({ 1.605 + ':method': (options.method || 'GET').toUpperCase(), 1.606 + ':scheme': (options.protocol && options.protocol.slice(0, -1)) || this._requestHeaders[':scheme'], 1.607 + ':authority': options.hostname || options.host || this._requestHeaders[':authority'], 1.608 + ':path': options.path 1.609 + }, options.headers); 1.610 + 1.611 + this._log.info({ method: promise[':method'], scheme: promise[':scheme'], 1.612 + authority: promise[':authority'], path: promise[':path'], 1.613 + headers: options.headers }, 'Promising push stream'); 1.614 + 1.615 + var pushStream = this.stream.promise(promise); 1.616 + 1.617 + return new OutgoingResponse(pushStream); 1.618 +}; 1.619 + 1.620 +// Overriding `EventEmitter`'s `on(event, listener)` method to forward certain subscriptions to 1.621 +// `request`. See `Server.prototype.on` for explanation. 1.622 +OutgoingResponse.prototype.on = function on(event, listener) { 1.623 + if (this.request && (event === 'timeout')) { 1.624 + this.request.on(event, listener && listener.bind(this)); 1.625 + } else { 1.626 + OutgoingMessage.prototype.on.call(this, event, listener); 1.627 + } 1.628 +}; 1.629 + 1.630 +// Client side 1.631 +// =========== 1.632 + 1.633 +exports.ClientRequest = OutgoingRequest; // for API compatibility 1.634 +exports.OutgoingRequest = OutgoingRequest; 1.635 +exports.IncomingResponse = IncomingResponse; 1.636 +exports.Agent = Agent; 1.637 +exports.globalAgent = undefined; 1.638 +exports.request = function request(options, callback) { 1.639 + return (options.agent || exports.globalAgent).request(options, callback); 1.640 +}; 1.641 +exports.get = function get(options, callback) { 1.642 + return (options.agent || exports.globalAgent).get(options, callback); 1.643 +}; 1.644 + 1.645 +// Agent class 1.646 +// ----------- 1.647 + 1.648 +function Agent(options) { 1.649 + EventEmitter.call(this); 1.650 + 1.651 + options = util._extend({}, options); 1.652 + 1.653 + this._settings = options.settings; 1.654 + this._log = (options.log || defaultLogger).child({ component: 'http' }); 1.655 + this.endpoints = {}; 1.656 + 1.657 + // * Using an own HTTPS agent, because the global agent does not look at `NPN/ALPNProtocols` when 1.658 + // generating the key identifying the connection, so we may get useless non-negotiated TLS 1.659 + // channels even if we ask for a negotiated one. This agent will contain only negotiated 1.660 + // channels. 1.661 + var agentOptions = {}; 1.662 + agentOptions.ALPNProtocols = supportedProtocols; 1.663 + agentOptions.NPNProtocols = supportedProtocols; 1.664 + this._httpsAgent = new https.Agent(agentOptions); 1.665 + 1.666 + this.sockets = this._httpsAgent.sockets; 1.667 + this.requests = this._httpsAgent.requests; 1.668 +} 1.669 +Agent.prototype = Object.create(EventEmitter.prototype, { constructor: { value: Agent } }); 1.670 + 1.671 +Agent.prototype.request = function request(options, callback) { 1.672 + if (typeof options === 'string') { 1.673 + options = url.parse(options); 1.674 + } else { 1.675 + options = util._extend({}, options); 1.676 + } 1.677 + 1.678 + options.method = (options.method || 'GET').toUpperCase(); 1.679 + options.protocol = options.protocol || 'https:'; 1.680 + options.host = options.hostname || options.host || 'localhost'; 1.681 + options.port = options.port || 443; 1.682 + options.path = options.path || '/'; 1.683 + 1.684 + if (!options.plain && options.protocol === 'http:') { 1.685 + this._log.error('Trying to negotiate client request with Upgrade from HTTP/1.1'); 1.686 + throw new Error('HTTP1.1 -> HTTP2 upgrade is not yet supported.'); 1.687 + } 1.688 + 1.689 + var request = new OutgoingRequest(this._log); 1.690 + 1.691 + if (callback) { 1.692 + request.on('response', callback); 1.693 + } 1.694 + 1.695 + var key = [ 1.696 + !!options.plain, 1.697 + options.host, 1.698 + options.port 1.699 + ].join(':'); 1.700 + 1.701 + // * There's an existing HTTP/2 connection to this host 1.702 + if (key in this.endpoints) { 1.703 + var endpoint = this.endpoints[key]; 1.704 + request._start(endpoint.createStream(), options); 1.705 + } 1.706 + 1.707 + // * HTTP/2 over plain TCP 1.708 + else if (options.plain) { 1.709 + endpoint = new Endpoint(this._log, 'CLIENT', this._settings); 1.710 + endpoint.socket = net.connect({ 1.711 + host: options.host, 1.712 + port: options.port, 1.713 + localAddress: options.localAddress 1.714 + }); 1.715 + endpoint.pipe(endpoint.socket).pipe(endpoint); 1.716 + request._start(endpoint.createStream(), options); 1.717 + } 1.718 + 1.719 + // * HTTP/2 over TLS negotiated using NPN or ALPN 1.720 + else { 1.721 + var started = false; 1.722 + options.ALPNProtocols = supportedProtocols; 1.723 + options.NPNProtocols = supportedProtocols; 1.724 + options.servername = options.host; // Server Name Indication 1.725 + options.agent = this._httpsAgent; 1.726 + var httpsRequest = https.request(options); 1.727 + 1.728 + httpsRequest.on('socket', function(socket) { 1.729 + var negotiatedProtocol = socket.alpnProtocol || socket.npnProtocol; 1.730 + if (negotiatedProtocol !== undefined) { 1.731 + negotiated(); 1.732 + } else { 1.733 + socket.on('secureConnect', negotiated); 1.734 + } 1.735 + }); 1.736 + 1.737 + var self = this; 1.738 + function negotiated() { 1.739 + var endpoint; 1.740 + var negotiatedProtocol = httpsRequest.socket.alpnProtocol || httpsRequest.socket.npnProtocol; 1.741 + if (negotiatedProtocol === implementedVersion) { 1.742 + httpsRequest.socket.emit('agentRemove'); 1.743 + unbundleSocket(httpsRequest.socket); 1.744 + endpoint = new Endpoint(self._log, 'CLIENT', self._settings); 1.745 + endpoint.socket = httpsRequest.socket; 1.746 + endpoint.pipe(endpoint.socket).pipe(endpoint); 1.747 + } 1.748 + if (started) { 1.749 + if (endpoint) { 1.750 + endpoint.close(); 1.751 + } else { 1.752 + httpsRequest.abort(); 1.753 + } 1.754 + } else { 1.755 + if (endpoint) { 1.756 + self._log.info({ e: endpoint, server: options.host + ':' + options.port }, 1.757 + 'New outgoing HTTP/2 connection'); 1.758 + self.endpoints[key] = endpoint; 1.759 + self.emit(key, endpoint); 1.760 + } else { 1.761 + self.emit(key, undefined); 1.762 + } 1.763 + } 1.764 + } 1.765 + 1.766 + this.once(key, function(endpoint) { 1.767 + started = true; 1.768 + if (endpoint) { 1.769 + request._start(endpoint.createStream(), options); 1.770 + } else { 1.771 + request._fallback(httpsRequest); 1.772 + } 1.773 + }); 1.774 + } 1.775 + 1.776 + return request; 1.777 +}; 1.778 + 1.779 +Agent.prototype.get = function get(options, callback) { 1.780 + var request = this.request(options, callback); 1.781 + request.end(); 1.782 + return request; 1.783 +}; 1.784 + 1.785 +function unbundleSocket(socket) { 1.786 + socket.removeAllListeners('data'); 1.787 + socket.removeAllListeners('end'); 1.788 + socket.removeAllListeners('readable'); 1.789 + socket.removeAllListeners('close'); 1.790 + socket.removeAllListeners('error'); 1.791 + socket.unpipe(); 1.792 + delete socket.ondata; 1.793 + delete socket.onend; 1.794 +} 1.795 + 1.796 +Object.defineProperty(Agent.prototype, 'maxSockets', { 1.797 + get: function getMaxSockets() { 1.798 + return this._httpsAgent.maxSockets; 1.799 + }, 1.800 + set: function setMaxSockets(value) { 1.801 + this._httpsAgent.maxSockets = value; 1.802 + } 1.803 +}); 1.804 + 1.805 +exports.globalAgent = new Agent(); 1.806 + 1.807 +// OutgoingRequest class 1.808 +// --------------------- 1.809 + 1.810 +function OutgoingRequest() { 1.811 + OutgoingMessage.call(this); 1.812 + 1.813 + this._log = undefined; 1.814 + 1.815 + this.stream = undefined; 1.816 +} 1.817 +OutgoingRequest.prototype = Object.create(OutgoingMessage.prototype, { constructor: { value: OutgoingRequest } }); 1.818 + 1.819 +OutgoingRequest.prototype._start = function _start(stream, options) { 1.820 + this.stream = stream; 1.821 + 1.822 + this._log = stream._log.child({ component: 'http' }); 1.823 + 1.824 + for (var key in options.headers) { 1.825 + this.setHeader(key, options.headers[key]); 1.826 + } 1.827 + var headers = this._headers; 1.828 + delete headers.host; 1.829 + 1.830 + if (options.auth) { 1.831 + headers.authorization = 'Basic ' + new Buffer(options.auth).toString('base64'); 1.832 + } 1.833 + 1.834 + headers[':scheme'] = options.protocol.slice(0, -1); 1.835 + headers[':method'] = options.method; 1.836 + headers[':authority'] = options.host; 1.837 + headers[':path'] = options.path; 1.838 + 1.839 + this._log.info({ scheme: headers[':scheme'], method: headers[':method'], 1.840 + authority: headers[':authority'], path: headers[':path'], 1.841 + headers: (options.headers || {}) }, 'Sending request'); 1.842 + this.stream.headers(headers); 1.843 + this.headersSent = true; 1.844 + 1.845 + this.emit('socket', this.stream); 1.846 + 1.847 + var response = new IncomingResponse(this.stream); 1.848 + response.once('ready', this.emit.bind(this, 'response', response)); 1.849 + 1.850 + this.stream.on('promise', this._onPromise.bind(this)); 1.851 +}; 1.852 + 1.853 +OutgoingRequest.prototype._fallback = function _fallback(request) { 1.854 + request.on('response', this.emit.bind(this, 'response')); 1.855 + this.stream = this.request = request; 1.856 + this.emit('socket', this.socket); 1.857 +}; 1.858 + 1.859 +OutgoingRequest.prototype.setPriority = function setPriority(priority) { 1.860 + if (this.stream) { 1.861 + this.stream.priority(priority); 1.862 + } else { 1.863 + this.once('socket', this.setPriority.bind(this, priority)); 1.864 + } 1.865 +}; 1.866 + 1.867 +// Overriding `EventEmitter`'s `on(event, listener)` method to forward certain subscriptions to 1.868 +// `request`. See `Server.prototype.on` for explanation. 1.869 +OutgoingRequest.prototype.on = function on(event, listener) { 1.870 + if (this.request && (event === 'upgrade')) { 1.871 + this.request.on(event, listener && listener.bind(this)); 1.872 + } else { 1.873 + OutgoingMessage.prototype.on.call(this, event, listener); 1.874 + } 1.875 +}; 1.876 + 1.877 +// Methods only in fallback mode 1.878 +OutgoingRequest.prototype.setNoDelay = function setNoDelay(noDelay) { 1.879 + if (this.request) { 1.880 + this.request.setNoDelay(noDelay); 1.881 + } else if (!this.stream) { 1.882 + this.on('socket', this.setNoDelay.bind(this, noDelay)); 1.883 + } 1.884 +}; 1.885 + 1.886 +OutgoingRequest.prototype.setSocketKeepAlive = function setSocketKeepAlive(enable, initialDelay) { 1.887 + if (this.request) { 1.888 + this.request.setSocketKeepAlive(enable, initialDelay); 1.889 + } else if (!this.stream) { 1.890 + this.on('socket', this.setSocketKeepAlive.bind(this, enable, initialDelay)); 1.891 + } 1.892 +}; 1.893 + 1.894 +OutgoingRequest.prototype.setTimeout = function setTimeout(timeout, callback) { 1.895 + if (this.request) { 1.896 + this.request.setTimeout(timeout, callback); 1.897 + } else if (!this.stream) { 1.898 + this.on('socket', this.setTimeout.bind(this, timeout, callback)); 1.899 + } 1.900 +}; 1.901 + 1.902 +// Aborting the request 1.903 +OutgoingRequest.prototype.abort = function abort() { 1.904 + if (this.request) { 1.905 + this.request.abort(); 1.906 + } else if (this.stream) { 1.907 + this.stream.reset('CANCEL'); 1.908 + } else { 1.909 + this.on('socket', this.abort.bind(this)); 1.910 + } 1.911 +}; 1.912 + 1.913 +// Receiving push promises 1.914 +OutgoingRequest.prototype._onPromise = function _onPromise(stream, headers) { 1.915 + this._log.info({ push_stream: stream.id }, 'Receiving push promise'); 1.916 + 1.917 + var promise = new IncomingPromise(stream, headers); 1.918 + 1.919 + if (this.listeners('push').length > 0) { 1.920 + this.emit('push', promise); 1.921 + } else { 1.922 + promise.cancel(); 1.923 + } 1.924 +}; 1.925 + 1.926 +// IncomingResponse class 1.927 +// ---------------------- 1.928 + 1.929 +function IncomingResponse(stream) { 1.930 + IncomingMessage.call(this, stream); 1.931 +} 1.932 +IncomingResponse.prototype = Object.create(IncomingMessage.prototype, { constructor: { value: IncomingResponse } }); 1.933 + 1.934 +// [Response Header Fields](http://tools.ietf.org/html/draft-ietf-httpbis-http2-10#section-8.1.3.2) 1.935 +// * `headers` argument: HTTP/2.0 request and response header fields carry information as a series 1.936 +// of key-value pairs. This includes the target URI for the request, the status code for the 1.937 +// response, as well as HTTP header fields. 1.938 +IncomingResponse.prototype._onHeaders = function _onHeaders(headers) { 1.939 + // * A single ":status" header field is defined that carries the HTTP status code field. This 1.940 + // header field MUST be included in all responses. 1.941 + // * A client MUST treat the absence of the ":status" header field, the presence of multiple 1.942 + // values, or an invalid value as a stream error of type PROTOCOL_ERROR. 1.943 + // Note: currently, we do not enforce it strictly: we accept any format, and parse it as int 1.944 + // * HTTP/2.0 does not define a way to carry the reason phrase that is included in an HTTP/1.1 1.945 + // status line. 1.946 + this.statusCode = parseInt(this._checkSpecialHeader(':status', headers[':status'])); 1.947 + 1.948 + // * Handling regular headers. 1.949 + IncomingMessage.prototype._onHeaders.call(this, headers); 1.950 + 1.951 + // * Signaling that the headers arrived. 1.952 + this._log.info({ status: this.statusCode, headers: this.headers}, 'Incoming response'); 1.953 + this.emit('ready'); 1.954 +}; 1.955 + 1.956 +// IncomingPromise class 1.957 +// ------------------------- 1.958 + 1.959 +function IncomingPromise(responseStream, promiseHeaders) { 1.960 + var stream = new Readable(); 1.961 + stream._read = noop; 1.962 + stream.push(null); 1.963 + stream._log = responseStream._log; 1.964 + 1.965 + IncomingRequest.call(this, stream); 1.966 + 1.967 + this._onHeaders(promiseHeaders); 1.968 + 1.969 + this._responseStream = responseStream; 1.970 + 1.971 + var response = new IncomingResponse(this._responseStream); 1.972 + response.once('ready', this.emit.bind(this, 'response', response)); 1.973 + 1.974 + this.stream.on('promise', this._onPromise.bind(this)); 1.975 +} 1.976 +IncomingPromise.prototype = Object.create(IncomingRequest.prototype, { constructor: { value: IncomingPromise } }); 1.977 + 1.978 +IncomingPromise.prototype.cancel = function cancel() { 1.979 + this._responseStream.reset('CANCEL'); 1.980 +}; 1.981 + 1.982 +IncomingPromise.prototype.setPriority = function setPriority(priority) { 1.983 + this._responseStream.priority(priority); 1.984 +}; 1.985 + 1.986 +IncomingPromise.prototype._onPromise = OutgoingRequest.prototype._onPromise;