Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
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; |