diff --git a/doc/api/http2.md b/doc/api/http2.md index ad40b37eb4f4ac..24af4e353d2dd8 100644 --- a/doc/api/http2.md +++ b/doc/api/http2.md @@ -1698,6 +1698,10 @@ changes: pr-url: https://github.com/nodejs/node/pull/16676 description: Added the `maxHeaderListPairs` option with a default limit of 128 header pairs. + - version: REPLACEME + pr-url: https://github.com/nodejs/node/pull/15752 + description: Added the `Http1IncomingMessage` and `Http1ServerResponse` + option. --> * `options` {Object} @@ -1747,6 +1751,12 @@ changes: used to determine the padding. See [Using options.selectPadding][]. * `settings` {HTTP/2 Settings Object} The initial settings to send to the remote peer upon connection. + * `Http1IncomingMessage` {http.IncomingMessage} Specifies the IncomingMessage + class to used for HTTP/1 fallback. Useful for extending the original + `http.IncomingMessage`. **Default:** `http.IncomingMessage` + * `Http1ServerResponse` {http.ServerResponse} Specifies the ServerResponse + class to used for HTTP/1 fallback. Useful for extending the original + `http.ServerResponse`. **Default:** `http.ServerResponse` * `onRequestHandler` {Function} See [Compatibility API][] * Returns: {Http2Server} diff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js index cbaf908246bd6d..361f4eeb64981d 100644 --- a/lib/internal/http2/core.js +++ b/lib/internal/http2/core.js @@ -5,6 +5,7 @@ require('internal/util').assertCrypto(); const { async_id_symbol } = process.binding('async_wrap'); +const http = require('http'); const binding = process.binding('http2'); const assert = require('assert'); const { Buffer } = require('buffer'); @@ -67,6 +68,8 @@ const NETServer = net.Server; const TLSServer = tls.Server; const kInspect = require('internal/util').customInspectSymbol; +const { kIncomingMessage } = require('_http_common'); +const { kServerResponse } = require('_http_server'); const kAlpnProtocol = Symbol('alpnProtocol'); const kAuthority = Symbol('authority'); @@ -2485,8 +2488,11 @@ function connectionListener(socket) { if (socket.alpnProtocol === false || socket.alpnProtocol === 'http/1.1') { // Fallback to HTTP/1.1 - if (options.allowHTTP1 === true) + if (options.allowHTTP1 === true) { + socket.server[kIncomingMessage] = options.Http1IncomingMessage; + socket.server[kServerResponse] = options.Http1ServerResponse; return httpConnectionListener.call(this, socket); + } // Let event handler deal with the socket debug(`Unknown protocol from ${socket.remoteAddress}:${socket.remotePort}`); if (!this.emit('unknownProtocol', socket)) { @@ -2526,6 +2532,13 @@ function initializeOptions(options) { options.allowHalfOpen = true; assertIsObject(options.settings, 'options.settings'); options.settings = Object.assign({}, options.settings); + + // Used only with allowHTTP1 + options.Http1IncomingMessage = options.Http1IncomingMessage || + http.IncomingMessage; + options.Http1ServerResponse = options.Http1ServerResponse || + http.ServerResponse; + return options; } diff --git a/test/parallel/test-http2-https-fallback-http-server-options.js b/test/parallel/test-http2-https-fallback-http-server-options.js new file mode 100644 index 00000000000000..20e2b122a24e8c --- /dev/null +++ b/test/parallel/test-http2-https-fallback-http-server-options.js @@ -0,0 +1,90 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const fixtures = require('../common/fixtures'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const url = require('url'); +const tls = require('tls'); +const http2 = require('http2'); +const https = require('https'); +const http = require('http'); + +const key = fixtures.readKey('agent8-key.pem'); +const cert = fixtures.readKey('agent8-cert.pem'); +const ca = fixtures.readKey('fake-startcom-root-cert.pem'); + +function onRequest(request, response) { + const { socket: { alpnProtocol } } = request.httpVersion === '2.0' ? + request.stream.session : request; + response.status(200); + response.end(JSON.stringify({ + alpnProtocol, + httpVersion: request.httpVersion, + userAgent: request.getUserAgent() + })); +} + +class MyIncomingMessage extends http.IncomingMessage { + getUserAgent() { + return this.headers['user-agent'] || 'unknown'; + } +} + +class MyServerResponse extends http.ServerResponse { + status(code) { + return this.writeHead(code, { 'Content-Type': 'application/json' }); + } +} + +// HTTP/2 & HTTP/1.1 server +{ + const server = http2.createSecureServer( + { + cert, + key, allowHTTP1: true, + Http1IncomingMessage: MyIncomingMessage, + Http1ServerResponse: MyServerResponse + }, + common.mustCall(onRequest, 1) + ); + + server.listen(0); + + server.on('listening', common.mustCall(() => { + const { port } = server.address(); + const origin = `https://localhost:${port}`; + + // HTTP/1.1 client + https.get( + Object.assign(url.parse(origin), { + secureContext: tls.createSecureContext({ ca }), + headers: { 'User-Agent': 'node-test' } + }), + common.mustCall((response) => { + assert.strictEqual(response.statusCode, 200); + assert.strictEqual(response.statusMessage, 'OK'); + assert.strictEqual( + response.headers['content-type'], + 'application/json' + ); + + response.setEncoding('utf8'); + let raw = ''; + response.on('data', (chunk) => { raw += chunk; }); + response.on('end', common.mustCall(() => { + const { alpnProtocol, httpVersion, userAgent } = JSON.parse(raw); + assert.strictEqual(alpnProtocol, false); + assert.strictEqual(httpVersion, '1.1'); + assert.strictEqual(userAgent, 'node-test'); + + server.close(); + })); + }) + ); + })); +}