diff --git a/doc/api/http2.md b/doc/api/http2.md index c28517c731..e6261ad62e 100644 --- a/doc/api/http2.md +++ b/doc/api/http2.md @@ -1598,6 +1598,9 @@ added: v8.4.0 used to determine the padding. See [Using options.selectPadding][]. * `settings` {[Settings Object][]} The initial settings to send to the remote peer upon connection. + * `createConnection` {Function} An optional callback that receives the `URL` + instance passed to `connect` and the `options` object, and returns any + [`Duplex`][] stream that is to be used as the connection for this session. * `listener` {Function} * Returns {Http2Session} diff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js index 4fa5d6e045..417d9fb869 100644 --- a/lib/internal/http2/core.js +++ b/lib/internal/http2/core.js @@ -13,6 +13,7 @@ const tls = require('tls'); const util = require('util'); const fs = require('fs'); const errors = require('internal/errors'); +const { StreamWrap } = require('_stream_wrap'); const { Duplex } = require('stream'); const { URL } = require('url'); const { onServerStream, @@ -683,10 +684,14 @@ class Http2Session extends EventEmitter { // type { number } either NGHTTP2_SESSION_SERVER or NGHTTP2_SESSION_CLIENT // options { Object } - // socket { net.Socket | tls.TLSSocket } + // socket { net.Socket | tls.TLSSocket | stream.Duplex } constructor(type, options, socket) { super(); + if (!socket._handle || !socket._handle._externalStream) { + socket = new StreamWrap(socket); + } + // No validation is performed on the input parameters because this // constructor is not exported directly for users. @@ -711,7 +716,8 @@ class Http2Session extends EventEmitter { this[kSocket] = socket; // Do not use nagle's algorithm - socket.setNoDelay(); + if (typeof socket.setNoDelay === 'function') + socket.setNoDelay(); // Disable TLS renegotiation on the socket if (typeof socket.disableRenegotiation === 'function') @@ -2417,15 +2423,19 @@ function connect(authority, options, listener) { const host = authority.hostname || authority.host || 'localhost'; let socket; - switch (protocol) { - case 'http:': - socket = net.connect(port, host); - break; - case 'https:': - socket = tls.connect(port, host, initializeTLSOptions(options, host)); - break; - default: - throw new errors.Error('ERR_HTTP2_UNSUPPORTED_PROTOCOL', protocol); + if (typeof options.createConnection === 'function') { + socket = options.createConnection(authority, options); + } else { + switch (protocol) { + case 'http:': + socket = net.connect(port, host); + break; + case 'https:': + socket = tls.connect(port, host, initializeTLSOptions(options, host)); + break; + default: + throw new errors.Error('ERR_HTTP2_UNSUPPORTED_PROTOCOL', protocol); + } } socket.on('error', socketOnError); diff --git a/test/parallel/test-http2-generic-streams-sendfile.js b/test/parallel/test-http2-generic-streams-sendfile.js new file mode 100644 index 0000000000..1054574a8b --- /dev/null +++ b/test/parallel/test-http2-generic-streams-sendfile.js @@ -0,0 +1,40 @@ +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const assert = require('assert'); +const http2 = require('http2'); +const fs = require('fs'); +const makeDuplexPair = require('../common/duplexpair'); + +{ + const server = http2.createServer(); + server.on('stream', common.mustCall((stream, headers) => { + stream.respondWithFile(__filename); + })); + + const { clientSide, serverSide } = makeDuplexPair(); + server.emit('connection', serverSide); + + const client = http2.connect('http://localhost:80', { + createConnection: common.mustCall(() => clientSide) + }); + + const req = client.request({ ':path': '/' }); + + req.on('response', common.mustCall((headers) => { + assert.strictEqual(headers[':status'], 200); + })); + + req.setEncoding('utf8'); + let data = ''; + req.on('data', (chunk) => { + data += chunk; + }); + req.on('end', common.mustCall(() => { + assert.strictEqual(data, fs.readFileSync(__filename, 'utf8')); + clientSide.destroy(); + clientSide.end(); + })); + req.end(); +} diff --git a/test/parallel/test-http2-generic-streams.js b/test/parallel/test-http2-generic-streams.js new file mode 100644 index 0000000000..d97e86a5ec --- /dev/null +++ b/test/parallel/test-http2-generic-streams.js @@ -0,0 +1,45 @@ +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const assert = require('assert'); +const http2 = require('http2'); +const makeDuplexPair = require('../common/duplexpair'); + +{ + const testData = '

Hello World

'; + const server = http2.createServer(); + server.on('stream', common.mustCall((stream, headers) => { + stream.respond({ + 'content-type': 'text/html', + ':status': 200 + }); + stream.end(testData); + })); + + const { clientSide, serverSide } = makeDuplexPair(); + server.emit('connection', serverSide); + + const client = http2.connect('http://localhost:80', { + createConnection: common.mustCall(() => clientSide) + }); + + const req = client.request({ ':path': '/' }); + + req.on('response', common.mustCall((headers) => { + assert.strictEqual(headers[':status'], 200); + })); + + req.setEncoding('utf8'); + // Note: This is checking that this small amount of data is passed through in + // a single chunk, which is unusual for our test suite but seems like a + // reasonable assumption here. + req.on('data', common.mustCall((data) => { + assert.strictEqual(data, testData); + })); + req.on('end', common.mustCall(() => { + clientSide.destroy(); + clientSide.end(); + })); + req.end(); +}