michael@0: // Public API michael@0: // ========== michael@0: michael@0: // The main governing power behind the http2 API design is that it should look very similar to the michael@0: // existing node.js [HTTPS API][1] (which is, in turn, almost identical to the [HTTP API][2]). The michael@0: // additional features of HTTP/2 are exposed as extensions to this API. Furthermore, node-http2 michael@0: // should fall back to using HTTP/1.1 if needed. Compatibility with undocumented or deprecated michael@0: // elements of the node.js HTTP/HTTPS API is a non-goal. michael@0: // michael@0: // Additional and modified API elements michael@0: // ------------------------------------ michael@0: // michael@0: // - **Class: http2.Endpoint**: an API for using the raw HTTP/2 framing layer. For documentation michael@0: // see the [lib/endpoint.js](endpoint.html) file. michael@0: // michael@0: // - **Class: http2.Server** michael@0: // - **Event: 'connection' (socket, [endpoint])**: there's a second argument if the negotiation of michael@0: // HTTP/2 was successful: the reference to the [Endpoint](endpoint.html) object tied to the michael@0: // socket. michael@0: // michael@0: // - **http2.createServer(options, [requestListener])**: additional option: michael@0: // - **log**: an optional [bunyan](https://github.com/trentm/node-bunyan) logger object michael@0: // - **plain**: if `true`, the server will accept HTTP/2 connections over plain TCP instead of michael@0: // TLS michael@0: // michael@0: // - **Class: http2.ServerResponse** michael@0: // - **response.push(options)**: initiates a server push. `options` describes the 'imaginary' michael@0: // request to which the push stream is a response; the possible options are identical to the michael@0: // ones accepted by `http2.request`. Returns a ServerResponse object that can be used to send michael@0: // the response headers and content. michael@0: // michael@0: // - **Class: http2.Agent** michael@0: // - **new Agent(options)**: additional option: michael@0: // - **log**: an optional [bunyan](https://github.com/trentm/node-bunyan) logger object michael@0: // - **agent.sockets**: only contains TCP sockets that corresponds to HTTP/1 requests. michael@0: // - **agent.endpoints**: contains [Endpoint](endpoint.html) objects for HTTP/2 connections. michael@0: // michael@0: // - **http2.request(options, [callback])**: additional option: michael@0: // - **plain**: if `true`, the client will not try to build a TLS tunnel, instead it will use michael@0: // the raw TCP stream for HTTP/2 michael@0: // michael@0: // - **Class: http2.ClientRequest** michael@0: // - **Event: 'socket' (socket)**: in case of an HTTP/2 incoming message, `socket` is a reference michael@0: // to the associated [HTTP/2 Stream](stream.html) object (and not to the TCP socket). michael@0: // - **Event: 'push' (promise)**: signals the intention of a server push associated to this michael@0: // request. `promise` is an IncomingPromise. If there's no listener for this event, the server michael@0: // push is cancelled. michael@0: // - **request.setPriority(priority)**: assign a priority to this request. `priority` is a number michael@0: // between 0 (highest priority) and 2^31-1 (lowest priority). Default value is 2^30. michael@0: // michael@0: // - **Class: http2.IncomingMessage** michael@0: // - has two subclasses for easier interface description: **IncomingRequest** and michael@0: // **IncomingResponse** michael@0: // - **message.socket**: in case of an HTTP/2 incoming message, it's a reference to the associated michael@0: // [HTTP/2 Stream](stream.html) object (and not to the TCP socket). michael@0: // michael@0: // - **Class: http2.IncomingRequest (IncomingMessage)** michael@0: // - **message.url**: in case of an HTTP/2 incoming request, the `url` field always contains the michael@0: // path, and never a full url (it contains the path in most cases in the HTTPS api as well). michael@0: // - **message.scheme**: additional field. Mandatory HTTP/2 request metadata. michael@0: // - **message.host**: additional field. Mandatory HTTP/2 request metadata. Note that this michael@0: // replaces the old Host header field, but node-http2 will add Host to the `message.headers` for michael@0: // backwards compatibility. michael@0: // michael@0: // - **Class: http2.IncomingPromise (IncomingRequest)** michael@0: // - contains the metadata of the 'imaginary' request to which the server push is an answer. michael@0: // - **Event: 'response' (response)**: signals the arrival of the actual push stream. `response` michael@0: // is an IncomingResponse. michael@0: // - **Event: 'push' (promise)**: signals the intention of a server push associated to this michael@0: // request. `promise` is an IncomingPromise. If there's no listener for this event, the server michael@0: // push is cancelled. michael@0: // - **promise.cancel()**: cancels the promised server push. michael@0: // - **promise.setPriority(priority)**: assign a priority to this push stream. `priority` is a michael@0: // number between 0 (highest priority) and 2^31-1 (lowest priority). Default value is 2^30. michael@0: // michael@0: // API elements not yet implemented michael@0: // -------------------------------- michael@0: // michael@0: // - **Class: http2.Server** michael@0: // - **server.maxHeadersCount** michael@0: // michael@0: // API elements that are not applicable to HTTP/2 michael@0: // ---------------------------------------------- michael@0: // michael@0: // The reason may be deprecation of certain HTTP/1.1 features, or that some API elements simply michael@0: // don't make sense when using HTTP/2. These will not be present when a request is done with HTTP/2, michael@0: // but will function normally when falling back to using HTTP/1.1. michael@0: // michael@0: // - **Class: http2.Server** michael@0: // - **Event: 'checkContinue'**: not in the spec, yet (see [http-spec#18][expect-continue]) michael@0: // - **Event: 'upgrade'**: upgrade is deprecated in HTTP/2 michael@0: // - **Event: 'timeout'**: HTTP/2 sockets won't timeout because of application level keepalive michael@0: // (PING frames) michael@0: // - **Event: 'connect'**: not in the spec, yet (see [http-spec#230][connect]) michael@0: // - **server.setTimeout(msecs, [callback])** michael@0: // - **server.timeout** michael@0: // michael@0: // - **Class: http2.ServerResponse** michael@0: // - **Event: 'close'** michael@0: // - **Event: 'timeout'** michael@0: // - **response.writeContinue()** michael@0: // - **response.writeHead(statusCode, [reasonPhrase], [headers])**: reasonPhrase will always be michael@0: // ignored since [it's not supported in HTTP/2][3] michael@0: // - **response.setTimeout(timeout, [callback])** michael@0: // michael@0: // - **Class: http2.Agent** michael@0: // - **agent.maxSockets**: only affects HTTP/1 connection pool. When using HTTP/2, there's always michael@0: // one connection per host. michael@0: // michael@0: // - **Class: http2.ClientRequest** michael@0: // - **Event: 'upgrade'** michael@0: // - **Event: 'connect'** michael@0: // - **Event: 'continue'** michael@0: // - **request.setTimeout(timeout, [callback])** michael@0: // - **request.setNoDelay([noDelay])** michael@0: // - **request.setSocketKeepAlive([enable], [initialDelay])** michael@0: // michael@0: // - **Class: http2.IncomingMessage** michael@0: // - **Event: 'close'** michael@0: // - **message.setTimeout(timeout, [callback])** michael@0: // michael@0: // [1]: http://nodejs.org/api/https.html michael@0: // [2]: http://nodejs.org/api/http.html michael@0: // [3]: http://tools.ietf.org/html/draft-ietf-httpbis-http2-10#section-8.1.3.2 michael@0: // [expect-continue]: https://github.com/http2/http2-spec/issues/18 michael@0: // [connect]: https://github.com/http2/http2-spec/issues/230 michael@0: michael@0: // Common server and client side code michael@0: // ================================== michael@0: michael@0: var net = require('net'); michael@0: var url = require('url'); michael@0: var util = require('util'); michael@0: var EventEmitter = require('events').EventEmitter; michael@0: var PassThrough = require('stream').PassThrough; michael@0: var Readable = require('stream').Readable; michael@0: var Writable = require('stream').Writable; michael@0: var Endpoint = require('http2-protocol').Endpoint; michael@0: var implementedVersion = require('http2-protocol').ImplementedVersion; michael@0: var http = require('http'); michael@0: var https = require('https'); michael@0: michael@0: exports.STATUS_CODES = http.STATUS_CODES; michael@0: exports.IncomingMessage = IncomingMessage; michael@0: exports.OutgoingMessage = OutgoingMessage; michael@0: michael@0: var deprecatedHeaders = [ michael@0: 'connection', michael@0: 'host', michael@0: 'keep-alive', michael@0: 'proxy-connection', michael@0: 'te', michael@0: 'transfer-encoding', michael@0: 'upgrade' michael@0: ]; michael@0: michael@0: // When doing NPN/ALPN negotiation, HTTP/1.1 is used as fallback michael@0: var supportedProtocols = [implementedVersion, 'http/1.1', 'http/1.0']; michael@0: michael@0: // Logging michael@0: // ------- michael@0: michael@0: // Logger shim, used when no logger is provided by the user. michael@0: function noop() {} michael@0: var defaultLogger = { michael@0: fatal: noop, michael@0: error: noop, michael@0: warn : noop, michael@0: info : noop, michael@0: debug: noop, michael@0: trace: noop, michael@0: michael@0: child: function() { return this; } michael@0: }; michael@0: michael@0: // Bunyan serializers exported by submodules that are worth adding when creating a logger. michael@0: exports.serializers = require('http2-protocol').serializers; michael@0: michael@0: // IncomingMessage class michael@0: // --------------------- michael@0: michael@0: function IncomingMessage(stream) { michael@0: // * This is basically a read-only wrapper for the [Stream](stream.html) class. michael@0: PassThrough.call(this); michael@0: stream.pipe(this); michael@0: this.socket = this.stream = stream; michael@0: michael@0: this._log = stream._log.child({ component: 'http' }); michael@0: michael@0: // * HTTP/2.0 does not define a way to carry the version identifier that is included in the michael@0: // HTTP/1.1 request/status line. Version is always 2.0. michael@0: this.httpVersion = '2.0'; michael@0: this.httpVersionMajor = 2; michael@0: this.httpVersionMinor = 0; michael@0: michael@0: // * `this.headers` will store the regular headers (and none of the special colon headers) michael@0: this.headers = {}; michael@0: this.trailers = undefined; michael@0: this._lastHeadersSeen = undefined; michael@0: michael@0: // * Other metadata is filled in when the headers arrive. michael@0: stream.once('headers', this._onHeaders.bind(this)); michael@0: stream.once('end', this._onEnd.bind(this)); michael@0: } michael@0: IncomingMessage.prototype = Object.create(PassThrough.prototype, { constructor: { value: IncomingMessage } }); michael@0: michael@0: // [Request Header Fields](http://tools.ietf.org/html/draft-ietf-httpbis-http2-10#section-8.1.3.1) michael@0: // * `headers` argument: HTTP/2.0 request and response header fields carry information as a series michael@0: // of key-value pairs. This includes the target URI for the request, the status code for the michael@0: // response, as well as HTTP header fields. michael@0: IncomingMessage.prototype._onHeaders = function _onHeaders(headers) { michael@0: // * An HTTP/2.0 request or response MUST NOT include any of the following header fields: michael@0: // Connection, Host, Keep-Alive, Proxy-Connection, TE, Transfer-Encoding, and Upgrade. A server michael@0: // MUST treat the presence of any of these header fields as a stream error of type michael@0: // PROTOCOL_ERROR. michael@0: for (var i = 0; i < deprecatedHeaders.length; i++) { michael@0: var key = deprecatedHeaders[i]; michael@0: if (key in headers) { michael@0: this._log.error({ key: key, value: headers[key] }, 'Deprecated header found'); michael@0: this.stream.emit('error', 'PROTOCOL_ERROR'); michael@0: return; michael@0: } michael@0: } michael@0: michael@0: // * Store the _regular_ headers in `this.headers` michael@0: for (var name in headers) { michael@0: if (name[0] !== ':') { michael@0: this.headers[name] = headers[name]; michael@0: } michael@0: } michael@0: michael@0: // * The last header block, if it's not the first, will represent the trailers michael@0: var self = this; michael@0: this.stream.on('headers', function(headers) { michael@0: self._lastHeadersSeen = headers; michael@0: }); michael@0: }; michael@0: michael@0: IncomingMessage.prototype._onEnd = function _onEnd() { michael@0: this.trailers = this._lastHeadersSeen; michael@0: }; michael@0: michael@0: IncomingMessage.prototype.setTimeout = noop; michael@0: michael@0: IncomingMessage.prototype._checkSpecialHeader = function _checkSpecialHeader(key, value) { michael@0: if ((typeof value !== 'string') || (value.length === 0)) { michael@0: this._log.error({ key: key, value: value }, 'Invalid or missing special header field'); michael@0: this.stream.emit('error', 'PROTOCOL_ERROR'); michael@0: } michael@0: michael@0: return value; michael@0: } michael@0: ; michael@0: michael@0: // OutgoingMessage class michael@0: // --------------------- michael@0: michael@0: function OutgoingMessage() { michael@0: // * This is basically a read-only wrapper for the [Stream](stream.html) class. michael@0: Writable.call(this); michael@0: michael@0: this._headers = {}; michael@0: this._trailers = undefined; michael@0: this.headersSent = false; michael@0: michael@0: this.on('finish', this._finish); michael@0: } michael@0: OutgoingMessage.prototype = Object.create(Writable.prototype, { constructor: { value: OutgoingMessage } }); michael@0: michael@0: OutgoingMessage.prototype._write = function _write(chunk, encoding, callback) { michael@0: if (this.stream) { michael@0: this.stream.write(chunk, encoding, callback); michael@0: } else { michael@0: this.once('socket', this._write.bind(this, chunk, encoding, callback)); michael@0: } michael@0: }; michael@0: michael@0: OutgoingMessage.prototype._finish = function _finish() { michael@0: if (this.stream) { michael@0: if (this._trailers) { michael@0: if (this.request) { michael@0: this.request.addTrailers(this._trailers); michael@0: } else { michael@0: this.stream.headers(this._trailers); michael@0: } michael@0: } michael@0: this.stream.end(); michael@0: } else { michael@0: this.once('socket', this._finish.bind(this)); michael@0: } michael@0: }; michael@0: michael@0: OutgoingMessage.prototype.setHeader = function setHeader(name, value) { michael@0: if (this.headersSent) { michael@0: throw new Error('Can\'t set headers after they are sent.'); michael@0: } else { michael@0: name = name.toLowerCase(); michael@0: if (deprecatedHeaders.indexOf(name) !== -1) { michael@0: throw new Error('Cannot set deprecated header: ' + name); michael@0: } michael@0: this._headers[name] = value; michael@0: } michael@0: }; michael@0: michael@0: OutgoingMessage.prototype.removeHeader = function removeHeader(name) { michael@0: if (this.headersSent) { michael@0: throw new Error('Can\'t remove headers after they are sent.'); michael@0: } else { michael@0: delete this._headers[name.toLowerCase()]; michael@0: } michael@0: }; michael@0: michael@0: OutgoingMessage.prototype.getHeader = function getHeader(name) { michael@0: return this._headers[name.toLowerCase()]; michael@0: }; michael@0: michael@0: OutgoingMessage.prototype.addTrailers = function addTrailers(trailers) { michael@0: this._trailers = trailers; michael@0: }; michael@0: michael@0: OutgoingMessage.prototype.setTimeout = noop; michael@0: michael@0: OutgoingMessage.prototype._checkSpecialHeader = IncomingMessage.prototype._checkSpecialHeader; michael@0: michael@0: // Server side michael@0: // =========== michael@0: michael@0: exports.createServer = createServer; michael@0: exports.Server = Server; michael@0: exports.IncomingRequest = IncomingRequest; michael@0: exports.OutgoingResponse = OutgoingResponse; michael@0: exports.ServerResponse = OutgoingResponse; // for API compatibility michael@0: michael@0: // Server class michael@0: // ------------ michael@0: michael@0: function Server(options) { michael@0: options = util._extend({}, options); michael@0: michael@0: this._log = (options.log || defaultLogger).child({ component: 'http' }); michael@0: this._settings = options.settings; michael@0: michael@0: var start = this._start.bind(this); michael@0: var fallback = this._fallback.bind(this); michael@0: michael@0: // HTTP2 over TLS (using NPN or ALPN) michael@0: if ((options.key && options.cert) || options.pfx) { michael@0: this._log.info('Creating HTTP/2 server over TLS'); michael@0: this._mode = 'tls'; michael@0: options.ALPNProtocols = supportedProtocols; michael@0: options.NPNProtocols = supportedProtocols; michael@0: this._server = https.createServer(options); michael@0: this._originalSocketListeners = this._server.listeners('secureConnection'); michael@0: this._server.removeAllListeners('secureConnection'); michael@0: this._server.on('secureConnection', function(socket) { michael@0: var negotiatedProtocol = socket.alpnProtocol || socket.npnProtocol; michael@0: if ((negotiatedProtocol === implementedVersion) && socket.servername) { michael@0: start(socket); michael@0: } else { michael@0: fallback(socket); michael@0: } michael@0: }); michael@0: this._server.on('request', this.emit.bind(this, 'request')); michael@0: } michael@0: michael@0: // HTTP2 over plain TCP michael@0: else if (options.plain) { michael@0: this._log.info('Creating HTTP/2 server over plain TCP'); michael@0: this._mode = 'plain'; michael@0: this._server = net.createServer(start); michael@0: } michael@0: michael@0: // HTTP/2 with HTTP/1.1 upgrade michael@0: else { michael@0: this._log.error('Trying to create HTTP/2 server with Upgrade from HTTP/1.1'); michael@0: throw new Error('HTTP1.1 -> HTTP2 upgrade is not yet supported. Please provide TLS keys.'); michael@0: } michael@0: michael@0: this._server.on('close', this.emit.bind(this, 'close')); michael@0: } michael@0: Server.prototype = Object.create(EventEmitter.prototype, { constructor: { value: Server } }); michael@0: michael@0: // Starting HTTP/2 michael@0: Server.prototype._start = function _start(socket) { michael@0: var endpoint = new Endpoint(this._log, 'SERVER', this._settings); michael@0: michael@0: this._log.info({ e: endpoint, michael@0: client: socket.remoteAddress + ':' + socket.remotePort, michael@0: SNI: socket.servername michael@0: }, 'New incoming HTTP/2 connection'); michael@0: michael@0: endpoint.pipe(socket).pipe(endpoint); michael@0: michael@0: var self = this; michael@0: endpoint.on('stream', function _onStream(stream) { michael@0: var response = new OutgoingResponse(stream); michael@0: var request = new IncomingRequest(stream); michael@0: michael@0: request.once('ready', self.emit.bind(self, 'request', request, response)); michael@0: }); michael@0: michael@0: endpoint.on('error', this.emit.bind(this, 'clientError')); michael@0: socket.on('error', this.emit.bind(this, 'clientError')); michael@0: michael@0: this.emit('connection', socket, endpoint); michael@0: }; michael@0: michael@0: Server.prototype._fallback = function _fallback(socket) { michael@0: var negotiatedProtocol = socket.alpnProtocol || socket.npnProtocol; michael@0: michael@0: this._log.info({ client: socket.remoteAddress + ':' + socket.remotePort, michael@0: protocol: negotiatedProtocol, michael@0: SNI: socket.servername michael@0: }, 'Falling back to simple HTTPS'); michael@0: michael@0: for (var i = 0; i < this._originalSocketListeners.length; i++) { michael@0: this._originalSocketListeners[i].call(this._server, socket); michael@0: } michael@0: michael@0: this.emit('connection', socket); michael@0: }; michael@0: michael@0: // There are [3 possible signatures][1] of the `listen` function. Every arguments is forwarded to michael@0: // the backing TCP or HTTPS server. michael@0: // [1]: http://nodejs.org/api/http.html#http_server_listen_port_hostname_backlog_callback michael@0: Server.prototype.listen = function listen(port, hostname) { michael@0: this._log.info({ on: ((typeof hostname === 'string') ? (hostname + ':' + port) : port) }, michael@0: 'Listening for incoming connections'); michael@0: this._server.listen.apply(this._server, arguments); michael@0: }; michael@0: michael@0: Server.prototype.close = function close(callback) { michael@0: this._log.info('Closing server'); michael@0: this._server.close(callback); michael@0: }; michael@0: michael@0: Server.prototype.setTimeout = function setTimeout(timeout, callback) { michael@0: if (this._mode === 'tls') { michael@0: this._server.setTimeout(timeout, callback); michael@0: } michael@0: }; michael@0: michael@0: Object.defineProperty(Server.prototype, 'timeout', { michael@0: get: function getTimeout() { michael@0: if (this._mode === 'tls') { michael@0: return this._server.timeout; michael@0: } else { michael@0: return undefined; michael@0: } michael@0: }, michael@0: set: function setTimeout(timeout) { michael@0: if (this._mode === 'tls') { michael@0: this._server.timeout = timeout; michael@0: } michael@0: } michael@0: }); michael@0: michael@0: // Overriding `EventEmitter`'s `on(event, listener)` method to forward certain subscriptions to michael@0: // `server`.There are events on the `http.Server` class where it makes difference whether someone is michael@0: // listening on the event or not. In these cases, we can not simply forward the events from the michael@0: // `server` to `this` since that means a listener. Instead, we forward the subscriptions. michael@0: Server.prototype.on = function on(event, listener) { michael@0: if ((event === 'upgrade') || (event === 'timeout')) { michael@0: this._server.on(event, listener && listener.bind(this)); michael@0: } else { michael@0: EventEmitter.prototype.on.call(this, event, listener); michael@0: } michael@0: }; michael@0: michael@0: // `addContext` is used to add Server Name Indication contexts michael@0: Server.prototype.addContext = function addContext(hostname, credentials) { michael@0: if (this._mode === 'tls') { michael@0: this._server.addContext(hostname, credentials); michael@0: } michael@0: }; michael@0: michael@0: function createServer(options, requestListener) { michael@0: if (typeof options === 'function') { michael@0: requestListener = options; michael@0: options = undefined; michael@0: } michael@0: michael@0: var server = new Server(options); michael@0: michael@0: if (requestListener) { michael@0: server.on('request', requestListener); michael@0: } michael@0: michael@0: return server; michael@0: } michael@0: michael@0: // IncomingRequest class michael@0: // --------------------- michael@0: michael@0: function IncomingRequest(stream) { michael@0: IncomingMessage.call(this, stream); michael@0: } michael@0: IncomingRequest.prototype = Object.create(IncomingMessage.prototype, { constructor: { value: IncomingRequest } }); michael@0: michael@0: // [Request Header Fields](http://tools.ietf.org/html/draft-ietf-httpbis-http2-10#section-8.1.3.1) michael@0: // * `headers` argument: HTTP/2.0 request and response header fields carry information as a series michael@0: // of key-value pairs. This includes the target URI for the request, the status code for the michael@0: // response, as well as HTTP header fields. michael@0: IncomingRequest.prototype._onHeaders = function _onHeaders(headers) { michael@0: // * The ":method" header field includes the HTTP method michael@0: // * The ":scheme" header field includes the scheme portion of the target URI michael@0: // * The ":authority" header field includes the authority portion of the target URI michael@0: // * The ":path" header field includes the path and query parts of the target URI. michael@0: // This field MUST NOT be empty; URIs that do not contain a path component MUST include a value michael@0: // of '/', unless the request is an OPTIONS request for '*', in which case the ":path" header michael@0: // field MUST include '*'. michael@0: // * All HTTP/2.0 requests MUST include exactly one valid value for all of these header fields. A michael@0: // server MUST treat the absence of any of these header fields, presence of multiple values, or michael@0: // an invalid value as a stream error of type PROTOCOL_ERROR. michael@0: this.method = this._checkSpecialHeader(':method' , headers[':method']); michael@0: this.scheme = this._checkSpecialHeader(':scheme' , headers[':scheme']); michael@0: this.host = this._checkSpecialHeader(':authority', headers[':authority'] ); michael@0: this.url = this._checkSpecialHeader(':path' , headers[':path'] ); michael@0: michael@0: // * Host header is included in the headers object for backwards compatibility. michael@0: this.headers.host = this.host; michael@0: michael@0: // * Handling regular headers. michael@0: IncomingMessage.prototype._onHeaders.call(this, headers); michael@0: michael@0: // * Signaling that the headers arrived. michael@0: this._log.info({ method: this.method, scheme: this.scheme, host: this.host, michael@0: path: this.url, headers: this.headers }, 'Incoming request'); michael@0: this.emit('ready'); michael@0: }; michael@0: michael@0: // OutgoingResponse class michael@0: // ---------------------- michael@0: michael@0: function OutgoingResponse(stream) { michael@0: OutgoingMessage.call(this); michael@0: michael@0: this._log = stream._log.child({ component: 'http' }); michael@0: michael@0: this.stream = stream; michael@0: this.statusCode = 200; michael@0: this.sendDate = true; michael@0: michael@0: this.stream.once('headers', this._onRequestHeaders.bind(this)); michael@0: } michael@0: OutgoingResponse.prototype = Object.create(OutgoingMessage.prototype, { constructor: { value: OutgoingResponse } }); michael@0: michael@0: OutgoingResponse.prototype.writeHead = function writeHead(statusCode, reasonPhrase, headers) { michael@0: if (typeof reasonPhrase === 'string') { michael@0: this._log.warn('Reason phrase argument was present but ignored by the writeHead method'); michael@0: } else { michael@0: headers = reasonPhrase; michael@0: } michael@0: michael@0: for (var name in headers) { michael@0: this.setHeader(name, headers[name]); michael@0: } michael@0: headers = this._headers; michael@0: michael@0: if (this.sendDate && !('date' in this._headers)) { michael@0: headers.date = (new Date()).toUTCString(); michael@0: } michael@0: michael@0: this._log.info({ status: statusCode, headers: this._headers }, 'Sending server response'); michael@0: michael@0: headers[':status'] = this.statusCode = statusCode; michael@0: michael@0: this.stream.headers(headers); michael@0: this.headersSent = true; michael@0: }; michael@0: michael@0: OutgoingResponse.prototype._implicitHeaders = function _implicitHeaders() { michael@0: if (!this.headersSent) { michael@0: this.writeHead(this.statusCode); michael@0: } michael@0: }; michael@0: michael@0: OutgoingResponse.prototype.write = function write() { michael@0: this._implicitHeaders(); michael@0: return OutgoingMessage.prototype.write.apply(this, arguments); michael@0: }; michael@0: michael@0: OutgoingResponse.prototype.end = function end() { michael@0: this._implicitHeaders(); michael@0: return OutgoingMessage.prototype.end.apply(this, arguments); michael@0: }; michael@0: michael@0: OutgoingResponse.prototype._onRequestHeaders = function _onRequestHeaders(headers) { michael@0: this._requestHeaders = headers; michael@0: }; michael@0: michael@0: OutgoingResponse.prototype.push = function push(options) { michael@0: if (typeof options === 'string') { michael@0: options = url.parse(options); michael@0: } michael@0: michael@0: if (!options.path) { michael@0: throw new Error('`path` option is mandatory.'); michael@0: } michael@0: michael@0: var promise = util._extend({ michael@0: ':method': (options.method || 'GET').toUpperCase(), michael@0: ':scheme': (options.protocol && options.protocol.slice(0, -1)) || this._requestHeaders[':scheme'], michael@0: ':authority': options.hostname || options.host || this._requestHeaders[':authority'], michael@0: ':path': options.path michael@0: }, options.headers); michael@0: michael@0: this._log.info({ method: promise[':method'], scheme: promise[':scheme'], michael@0: authority: promise[':authority'], path: promise[':path'], michael@0: headers: options.headers }, 'Promising push stream'); michael@0: michael@0: var pushStream = this.stream.promise(promise); michael@0: michael@0: return new OutgoingResponse(pushStream); michael@0: }; michael@0: michael@0: // Overriding `EventEmitter`'s `on(event, listener)` method to forward certain subscriptions to michael@0: // `request`. See `Server.prototype.on` for explanation. michael@0: OutgoingResponse.prototype.on = function on(event, listener) { michael@0: if (this.request && (event === 'timeout')) { michael@0: this.request.on(event, listener && listener.bind(this)); michael@0: } else { michael@0: OutgoingMessage.prototype.on.call(this, event, listener); michael@0: } michael@0: }; michael@0: michael@0: // Client side michael@0: // =========== michael@0: michael@0: exports.ClientRequest = OutgoingRequest; // for API compatibility michael@0: exports.OutgoingRequest = OutgoingRequest; michael@0: exports.IncomingResponse = IncomingResponse; michael@0: exports.Agent = Agent; michael@0: exports.globalAgent = undefined; michael@0: exports.request = function request(options, callback) { michael@0: return (options.agent || exports.globalAgent).request(options, callback); michael@0: }; michael@0: exports.get = function get(options, callback) { michael@0: return (options.agent || exports.globalAgent).get(options, callback); michael@0: }; michael@0: michael@0: // Agent class michael@0: // ----------- michael@0: michael@0: function Agent(options) { michael@0: EventEmitter.call(this); michael@0: michael@0: options = util._extend({}, options); michael@0: michael@0: this._settings = options.settings; michael@0: this._log = (options.log || defaultLogger).child({ component: 'http' }); michael@0: this.endpoints = {}; michael@0: michael@0: // * Using an own HTTPS agent, because the global agent does not look at `NPN/ALPNProtocols` when michael@0: // generating the key identifying the connection, so we may get useless non-negotiated TLS michael@0: // channels even if we ask for a negotiated one. This agent will contain only negotiated michael@0: // channels. michael@0: var agentOptions = {}; michael@0: agentOptions.ALPNProtocols = supportedProtocols; michael@0: agentOptions.NPNProtocols = supportedProtocols; michael@0: this._httpsAgent = new https.Agent(agentOptions); michael@0: michael@0: this.sockets = this._httpsAgent.sockets; michael@0: this.requests = this._httpsAgent.requests; michael@0: } michael@0: Agent.prototype = Object.create(EventEmitter.prototype, { constructor: { value: Agent } }); michael@0: michael@0: Agent.prototype.request = function request(options, callback) { michael@0: if (typeof options === 'string') { michael@0: options = url.parse(options); michael@0: } else { michael@0: options = util._extend({}, options); michael@0: } michael@0: michael@0: options.method = (options.method || 'GET').toUpperCase(); michael@0: options.protocol = options.protocol || 'https:'; michael@0: options.host = options.hostname || options.host || 'localhost'; michael@0: options.port = options.port || 443; michael@0: options.path = options.path || '/'; michael@0: michael@0: if (!options.plain && options.protocol === 'http:') { michael@0: this._log.error('Trying to negotiate client request with Upgrade from HTTP/1.1'); michael@0: throw new Error('HTTP1.1 -> HTTP2 upgrade is not yet supported.'); michael@0: } michael@0: michael@0: var request = new OutgoingRequest(this._log); michael@0: michael@0: if (callback) { michael@0: request.on('response', callback); michael@0: } michael@0: michael@0: var key = [ michael@0: !!options.plain, michael@0: options.host, michael@0: options.port michael@0: ].join(':'); michael@0: michael@0: // * There's an existing HTTP/2 connection to this host michael@0: if (key in this.endpoints) { michael@0: var endpoint = this.endpoints[key]; michael@0: request._start(endpoint.createStream(), options); michael@0: } michael@0: michael@0: // * HTTP/2 over plain TCP michael@0: else if (options.plain) { michael@0: endpoint = new Endpoint(this._log, 'CLIENT', this._settings); michael@0: endpoint.socket = net.connect({ michael@0: host: options.host, michael@0: port: options.port, michael@0: localAddress: options.localAddress michael@0: }); michael@0: endpoint.pipe(endpoint.socket).pipe(endpoint); michael@0: request._start(endpoint.createStream(), options); michael@0: } michael@0: michael@0: // * HTTP/2 over TLS negotiated using NPN or ALPN michael@0: else { michael@0: var started = false; michael@0: options.ALPNProtocols = supportedProtocols; michael@0: options.NPNProtocols = supportedProtocols; michael@0: options.servername = options.host; // Server Name Indication michael@0: options.agent = this._httpsAgent; michael@0: var httpsRequest = https.request(options); michael@0: michael@0: httpsRequest.on('socket', function(socket) { michael@0: var negotiatedProtocol = socket.alpnProtocol || socket.npnProtocol; michael@0: if (negotiatedProtocol !== undefined) { michael@0: negotiated(); michael@0: } else { michael@0: socket.on('secureConnect', negotiated); michael@0: } michael@0: }); michael@0: michael@0: var self = this; michael@0: function negotiated() { michael@0: var endpoint; michael@0: var negotiatedProtocol = httpsRequest.socket.alpnProtocol || httpsRequest.socket.npnProtocol; michael@0: if (negotiatedProtocol === implementedVersion) { michael@0: httpsRequest.socket.emit('agentRemove'); michael@0: unbundleSocket(httpsRequest.socket); michael@0: endpoint = new Endpoint(self._log, 'CLIENT', self._settings); michael@0: endpoint.socket = httpsRequest.socket; michael@0: endpoint.pipe(endpoint.socket).pipe(endpoint); michael@0: } michael@0: if (started) { michael@0: if (endpoint) { michael@0: endpoint.close(); michael@0: } else { michael@0: httpsRequest.abort(); michael@0: } michael@0: } else { michael@0: if (endpoint) { michael@0: self._log.info({ e: endpoint, server: options.host + ':' + options.port }, michael@0: 'New outgoing HTTP/2 connection'); michael@0: self.endpoints[key] = endpoint; michael@0: self.emit(key, endpoint); michael@0: } else { michael@0: self.emit(key, undefined); michael@0: } michael@0: } michael@0: } michael@0: michael@0: this.once(key, function(endpoint) { michael@0: started = true; michael@0: if (endpoint) { michael@0: request._start(endpoint.createStream(), options); michael@0: } else { michael@0: request._fallback(httpsRequest); michael@0: } michael@0: }); michael@0: } michael@0: michael@0: return request; michael@0: }; michael@0: michael@0: Agent.prototype.get = function get(options, callback) { michael@0: var request = this.request(options, callback); michael@0: request.end(); michael@0: return request; michael@0: }; michael@0: michael@0: function unbundleSocket(socket) { michael@0: socket.removeAllListeners('data'); michael@0: socket.removeAllListeners('end'); michael@0: socket.removeAllListeners('readable'); michael@0: socket.removeAllListeners('close'); michael@0: socket.removeAllListeners('error'); michael@0: socket.unpipe(); michael@0: delete socket.ondata; michael@0: delete socket.onend; michael@0: } michael@0: michael@0: Object.defineProperty(Agent.prototype, 'maxSockets', { michael@0: get: function getMaxSockets() { michael@0: return this._httpsAgent.maxSockets; michael@0: }, michael@0: set: function setMaxSockets(value) { michael@0: this._httpsAgent.maxSockets = value; michael@0: } michael@0: }); michael@0: michael@0: exports.globalAgent = new Agent(); michael@0: michael@0: // OutgoingRequest class michael@0: // --------------------- michael@0: michael@0: function OutgoingRequest() { michael@0: OutgoingMessage.call(this); michael@0: michael@0: this._log = undefined; michael@0: michael@0: this.stream = undefined; michael@0: } michael@0: OutgoingRequest.prototype = Object.create(OutgoingMessage.prototype, { constructor: { value: OutgoingRequest } }); michael@0: michael@0: OutgoingRequest.prototype._start = function _start(stream, options) { michael@0: this.stream = stream; michael@0: michael@0: this._log = stream._log.child({ component: 'http' }); michael@0: michael@0: for (var key in options.headers) { michael@0: this.setHeader(key, options.headers[key]); michael@0: } michael@0: var headers = this._headers; michael@0: delete headers.host; michael@0: michael@0: if (options.auth) { michael@0: headers.authorization = 'Basic ' + new Buffer(options.auth).toString('base64'); michael@0: } michael@0: michael@0: headers[':scheme'] = options.protocol.slice(0, -1); michael@0: headers[':method'] = options.method; michael@0: headers[':authority'] = options.host; michael@0: headers[':path'] = options.path; michael@0: michael@0: this._log.info({ scheme: headers[':scheme'], method: headers[':method'], michael@0: authority: headers[':authority'], path: headers[':path'], michael@0: headers: (options.headers || {}) }, 'Sending request'); michael@0: this.stream.headers(headers); michael@0: this.headersSent = true; michael@0: michael@0: this.emit('socket', this.stream); michael@0: michael@0: var response = new IncomingResponse(this.stream); michael@0: response.once('ready', this.emit.bind(this, 'response', response)); michael@0: michael@0: this.stream.on('promise', this._onPromise.bind(this)); michael@0: }; michael@0: michael@0: OutgoingRequest.prototype._fallback = function _fallback(request) { michael@0: request.on('response', this.emit.bind(this, 'response')); michael@0: this.stream = this.request = request; michael@0: this.emit('socket', this.socket); michael@0: }; michael@0: michael@0: OutgoingRequest.prototype.setPriority = function setPriority(priority) { michael@0: if (this.stream) { michael@0: this.stream.priority(priority); michael@0: } else { michael@0: this.once('socket', this.setPriority.bind(this, priority)); michael@0: } michael@0: }; michael@0: michael@0: // Overriding `EventEmitter`'s `on(event, listener)` method to forward certain subscriptions to michael@0: // `request`. See `Server.prototype.on` for explanation. michael@0: OutgoingRequest.prototype.on = function on(event, listener) { michael@0: if (this.request && (event === 'upgrade')) { michael@0: this.request.on(event, listener && listener.bind(this)); michael@0: } else { michael@0: OutgoingMessage.prototype.on.call(this, event, listener); michael@0: } michael@0: }; michael@0: michael@0: // Methods only in fallback mode michael@0: OutgoingRequest.prototype.setNoDelay = function setNoDelay(noDelay) { michael@0: if (this.request) { michael@0: this.request.setNoDelay(noDelay); michael@0: } else if (!this.stream) { michael@0: this.on('socket', this.setNoDelay.bind(this, noDelay)); michael@0: } michael@0: }; michael@0: michael@0: OutgoingRequest.prototype.setSocketKeepAlive = function setSocketKeepAlive(enable, initialDelay) { michael@0: if (this.request) { michael@0: this.request.setSocketKeepAlive(enable, initialDelay); michael@0: } else if (!this.stream) { michael@0: this.on('socket', this.setSocketKeepAlive.bind(this, enable, initialDelay)); michael@0: } michael@0: }; michael@0: michael@0: OutgoingRequest.prototype.setTimeout = function setTimeout(timeout, callback) { michael@0: if (this.request) { michael@0: this.request.setTimeout(timeout, callback); michael@0: } else if (!this.stream) { michael@0: this.on('socket', this.setTimeout.bind(this, timeout, callback)); michael@0: } michael@0: }; michael@0: michael@0: // Aborting the request michael@0: OutgoingRequest.prototype.abort = function abort() { michael@0: if (this.request) { michael@0: this.request.abort(); michael@0: } else if (this.stream) { michael@0: this.stream.reset('CANCEL'); michael@0: } else { michael@0: this.on('socket', this.abort.bind(this)); michael@0: } michael@0: }; michael@0: michael@0: // Receiving push promises michael@0: OutgoingRequest.prototype._onPromise = function _onPromise(stream, headers) { michael@0: this._log.info({ push_stream: stream.id }, 'Receiving push promise'); michael@0: michael@0: var promise = new IncomingPromise(stream, headers); michael@0: michael@0: if (this.listeners('push').length > 0) { michael@0: this.emit('push', promise); michael@0: } else { michael@0: promise.cancel(); michael@0: } michael@0: }; michael@0: michael@0: // IncomingResponse class michael@0: // ---------------------- michael@0: michael@0: function IncomingResponse(stream) { michael@0: IncomingMessage.call(this, stream); michael@0: } michael@0: IncomingResponse.prototype = Object.create(IncomingMessage.prototype, { constructor: { value: IncomingResponse } }); michael@0: michael@0: // [Response Header Fields](http://tools.ietf.org/html/draft-ietf-httpbis-http2-10#section-8.1.3.2) michael@0: // * `headers` argument: HTTP/2.0 request and response header fields carry information as a series michael@0: // of key-value pairs. This includes the target URI for the request, the status code for the michael@0: // response, as well as HTTP header fields. michael@0: IncomingResponse.prototype._onHeaders = function _onHeaders(headers) { michael@0: // * A single ":status" header field is defined that carries the HTTP status code field. This michael@0: // header field MUST be included in all responses. michael@0: // * A client MUST treat the absence of the ":status" header field, the presence of multiple michael@0: // values, or an invalid value as a stream error of type PROTOCOL_ERROR. michael@0: // Note: currently, we do not enforce it strictly: we accept any format, and parse it as int michael@0: // * HTTP/2.0 does not define a way to carry the reason phrase that is included in an HTTP/1.1 michael@0: // status line. michael@0: this.statusCode = parseInt(this._checkSpecialHeader(':status', headers[':status'])); michael@0: michael@0: // * Handling regular headers. michael@0: IncomingMessage.prototype._onHeaders.call(this, headers); michael@0: michael@0: // * Signaling that the headers arrived. michael@0: this._log.info({ status: this.statusCode, headers: this.headers}, 'Incoming response'); michael@0: this.emit('ready'); michael@0: }; michael@0: michael@0: // IncomingPromise class michael@0: // ------------------------- michael@0: michael@0: function IncomingPromise(responseStream, promiseHeaders) { michael@0: var stream = new Readable(); michael@0: stream._read = noop; michael@0: stream.push(null); michael@0: stream._log = responseStream._log; michael@0: michael@0: IncomingRequest.call(this, stream); michael@0: michael@0: this._onHeaders(promiseHeaders); michael@0: michael@0: this._responseStream = responseStream; michael@0: michael@0: var response = new IncomingResponse(this._responseStream); michael@0: response.once('ready', this.emit.bind(this, 'response', response)); michael@0: michael@0: this.stream.on('promise', this._onPromise.bind(this)); michael@0: } michael@0: IncomingPromise.prototype = Object.create(IncomingRequest.prototype, { constructor: { value: IncomingPromise } }); michael@0: michael@0: IncomingPromise.prototype.cancel = function cancel() { michael@0: this._responseStream.reset('CANCEL'); michael@0: }; michael@0: michael@0: IncomingPromise.prototype.setPriority = function setPriority(priority) { michael@0: this._responseStream.priority(priority); michael@0: }; michael@0: michael@0: IncomingPromise.prototype._onPromise = OutgoingRequest.prototype._onPromise;