From 335d9a963268f99d74288b7f61579f8ee30d507f Mon Sep 17 00:00:00 2001 From: Momtchil Momtchev Date: Fri, 16 Oct 2020 13:41:09 +0200 Subject: [PATCH] http2: reinject data received before http2 is attached Reinject the data already received from the TLS socket when the HTTP2 client is attached with a delay Fixes: https://github.com/nodejs/node/issues/35475 PR-URL: https://github.com/nodejs/node/pull/35678 Reviewed-By: Anna Henningsen Reviewed-By: Matteo Collina Reviewed-By: Rich Trott Reviewed-By: Alba Mendez Reviewed-By: Franziska Hinkelmann Reviewed-By: Ricky Zhou <0x19951125@gmail.com> --- lib/internal/http2/core.js | 15 ++++- src/node_http2.cc | 28 ++++++++ src/node_http2.h | 1 + .../test-http2-connect-tls-with-delay.js | 64 +++++++++++++++++++ 4 files changed, 106 insertions(+), 2 deletions(-) create mode 100644 test/parallel/test-http2-connect-tls-with-delay.js diff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js index 8a29b82cd69c73..6f6fcbd66038ac 100644 --- a/lib/internal/http2/core.js +++ b/lib/internal/http2/core.js @@ -1028,7 +1028,7 @@ function finishSessionClose(session, error) { if (socket && !socket.destroyed) { // Always wait for writable side to finish. socket.end((err) => { - debugSessionObj(session, 'finishSessionClose socket end', err); + debugSessionObj(session, 'finishSessionClose socket end', err, error); // Due to the way the underlying stream is handled in Http2Session we // won't get graceful Readable end from the other side even if it was sent // as the stream is already considered closed and will neither be read @@ -1046,7 +1046,7 @@ function finishSessionClose(session, error) { } function closeSession(session, code, error) { - debugSessionObj(session, 'start closing/destroying'); + debugSessionObj(session, 'start closing/destroying', error); const state = session[kState]; state.flags |= SESSION_FLAGS_DESTROYED; @@ -3106,6 +3106,17 @@ function connect(authority, options, listener) { if (typeof listener === 'function') session.once('connect', listener); + + debug('Http2Session connect', options.createConnection); + // Socket already has some buffered data - emulate receiving it + // https://github.com/nodejs/node/issues/35475 + if (typeof options.createConnection === 'function') { + let buf; + while ((buf = socket.read()) !== null) { + debug(`Http2Session connect: injecting ${buf.length} already in buffer`); + session[kHandle].receive(buf); + } + } return session; } diff --git a/src/node_http2.cc b/src/node_http2.cc index fc345e52b23e2e..fc8385a39fa7ff 100644 --- a/src/node_http2.cc +++ b/src/node_http2.cc @@ -1822,6 +1822,33 @@ void Http2Session::Consume(Local stream_obj) { Debug(this, "i/o stream consumed"); } +// Allow injecting of data from JS +// This is used when the socket has already some data received +// before our listener was attached +// https://github.com/nodejs/node/issues/35475 +void Http2Session::Receive(const FunctionCallbackInfo& args) { + Http2Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + CHECK(args[0]->IsObject()); + + ArrayBufferViewContents buffer(args[0]); + const char* data = buffer.data(); + size_t len = buffer.length(); + Debug(session, "Receiving %zu bytes injected from JS", len); + + // Copy given buffer + while (len > 0) { + uv_buf_t buf = session->OnStreamAlloc(len); + size_t copy = buf.len > len ? len : buf.len; + memcpy(buf.base, data, copy); + buf.len = copy; + session->OnStreamRead(copy, buf); + + data += copy; + len -= copy; + } +} + Http2Stream* Http2Stream::New(Http2Session* session, int32_t id, nghttp2_headers_category category, @@ -3047,6 +3074,7 @@ void Initialize(Local target, env->SetProtoMethod(session, "altsvc", Http2Session::AltSvc); env->SetProtoMethod(session, "ping", Http2Session::Ping); env->SetProtoMethod(session, "consume", Http2Session::Consume); + env->SetProtoMethod(session, "receive", Http2Session::Receive); env->SetProtoMethod(session, "destroy", Http2Session::Destroy); env->SetProtoMethod(session, "goaway", Http2Session::Goaway); env->SetProtoMethod(session, "settings", Http2Session::Settings); diff --git a/src/node_http2.h b/src/node_http2.h index e49c3a60f32bcc..417318b1b49c70 100644 --- a/src/node_http2.h +++ b/src/node_http2.h @@ -695,6 +695,7 @@ class Http2Session : public AsyncWrap, // The JavaScript API static void New(const v8::FunctionCallbackInfo& args); static void Consume(const v8::FunctionCallbackInfo& args); + static void Receive(const v8::FunctionCallbackInfo& args); static void Destroy(const v8::FunctionCallbackInfo& args); static void Settings(const v8::FunctionCallbackInfo& args); static void Request(const v8::FunctionCallbackInfo& args); diff --git a/test/parallel/test-http2-connect-tls-with-delay.js b/test/parallel/test-http2-connect-tls-with-delay.js new file mode 100644 index 00000000000000..3e2e8a46a3662a --- /dev/null +++ b/test/parallel/test-http2-connect-tls-with-delay.js @@ -0,0 +1,64 @@ +// Flags: --expose-internals +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +if (!common.hasMultiLocalhost()) + common.skip('platform-specific test.'); + +const http2 = require('http2'); +const assert = require('assert'); +const tls = require('tls'); +const fixtures = require('../common/fixtures'); + +const serverOptions = { + key: fixtures.readKey('agent1-key.pem'), + cert: fixtures.readKey('agent1-cert.pem') +}; +const server = http2.createSecureServer(serverOptions, (req, res) => { + console.log(`Connect from: ${req.connection.remoteAddress}`); + assert.strictEqual(req.connection.remoteAddress, '127.0.0.2'); + + req.on('end', common.mustCall(() => { + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end(`You are from: ${req.connection.remoteAddress}`); + })); + req.resume(); +}); + +server.listen(0, '127.0.0.1', common.mustCall(() => { + const options = { + ALPNProtocols: ['h2'], + host: '127.0.0.1', + servername: 'localhost', + localAddress: '127.0.0.2', + port: server.address().port, + rejectUnauthorized: false + }; + + console.log('Server ready', server.address().port); + + const socket = tls.connect(options, async () => { + + console.log('TLS Connected!'); + + setTimeout(() => { + + const client = http2.connect( + 'https://localhost:' + server.address().port, + { ...options, createConnection: () => socket } + ); + const req = client.request({ + ':path': '/' + }); + req.on('data', () => req.resume()); + req.on('end', common.mustCall(function() { + client.close(); + req.close(); + server.close(); + })); + req.end(); + }, 1000); + }); +}));