Skip to content

Commit 5c37a73

Browse files
vtjnashmmomtchev
andcommitted
TLS: improve compliance with shutdown standard
RFC 5246 section-7.2.1 requires that the implementation must immediately stop using the stream, as it is no longer TLS-encrypted. The stream is permitted to still pump events (and errors) to other users, but those are now unencrypted, so we should not process them here. But therefore, we do not want to stop the underlying stream, as there could be another user of it, but we do remove ourselves as a listener. The section also states that the application must destroy the stream immediately (discarding any pending writes, and sending a close_notify response back), but we leave that to the upper layer of the application here, as it should be sufficient to permit standards compliant usage just to be ignoring read events. Fixes: nodejs/node#35904 Closes: nodejs/node#35946 Co-authored-by: Momtchil Momtchev <[email protected]>
1 parent 4b0308a commit 5c37a73

File tree

4 files changed

+73
-23
lines changed

4 files changed

+73
-23
lines changed

lib/_http_client.js

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -409,11 +409,6 @@ function socketCloseListener() {
409409
const req = socket._httpMessage;
410410
debug('HTTP socket close');
411411

412-
// Pull through final chunk, if anything is buffered.
413-
// the ondata function will handle it properly, and this
414-
// is a no-op if no final chunk remains.
415-
socket.read();
416-
417412
// NOTE: It's important to get parser here, because it could be freed by
418413
// the `socketOnData`.
419414
const parser = socket.parser;

lib/internal/stream_base_commons.js

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,9 @@ function onStreamRead(arrayBuffer) {
207207
return;
208208
}
209209

210+
// note: handle should never deliver any more read events after this point.
211+
// (equivalently, it should have called readStop on itself alread).
212+
210213
if (nread !== UV_EOF) {
211214
// CallJSOnreadMethod expects the return value to be a buffer.
212215
// Ref: https://github.com/nodejs/node/pull/34375
@@ -222,20 +225,6 @@ function onStreamRead(arrayBuffer) {
222225
if (stream[kMaybeDestroy])
223226
stream.on('end', stream[kMaybeDestroy]);
224227

225-
// TODO(ronag): Without this `readStop`, `onStreamRead`
226-
// will be called once more (i.e. after Readable.ended)
227-
// on Windows causing a ECONNRESET, failing the
228-
// test-https-truncate test.
229-
if (handle.readStop) {
230-
const err = handle.readStop();
231-
if (err) {
232-
// CallJSOnreadMethod expects the return value to be a buffer.
233-
// Ref: https://github.com/nodejs/node/pull/34375
234-
stream.destroy(errnoException(err, 'read'));
235-
return;
236-
}
237-
}
238-
239228
// Push a null to signal the end of data.
240229
// Do it before `maybeDestroy` for correct order of events:
241230
// `end` -> `close`

src/crypto/crypto_tls.cc

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -808,6 +808,11 @@ void TLSWrap::ClearOut() {
808808
int flags = SSL_get_shutdown(ssl_.get());
809809
if (!eof_ && flags & SSL_RECEIVED_SHUTDOWN) {
810810
eof_ = true;
811+
// FIXME: rfc5246#section-7.2.1 says we should call Destroy
812+
// here, but Shutdown and Destroy in this class also affect the
813+
// underlying stream. That behavior is permitted by the
814+
// standard of the application protocol, but not of nodejs at
815+
// the protocol layer in this file.
811816
EmitRead(UV_EOF);
812817
}
813818

@@ -921,7 +926,7 @@ bool TLSWrap::IsClosing() {
921926

922927
int TLSWrap::ReadStart() {
923928
Debug(this, "ReadStart()");
924-
if (underlying_stream() != nullptr)
929+
if (underlying_stream() != nullptr && !eof_)
925930
return underlying_stream()->ReadStart();
926931
return 0;
927932
}
@@ -1080,14 +1085,18 @@ uv_buf_t TLSWrap::OnStreamAlloc(size_t suggested_size) {
10801085

10811086
void TLSWrap::OnStreamRead(ssize_t nread, const uv_buf_t& buf) {
10821087
Debug(this, "Read %zd bytes from underlying stream", nread);
1088+
1089+
// Ignore everything after close_notify (rfc5246#section-7.2.1)
1090+
// TODO: unregister our interest in read events
1091+
if (eof_)
1092+
return;
1093+
10831094
if (nread < 0) {
10841095
// Error should be emitted only after all data was read
10851096
ClearOut();
10861097

1087-
// Ignore EOF if received close_notify
10881098
if (nread == UV_EOF) {
1089-
if (eof_)
1090-
return;
1099+
// underlying stream already should have also called ReadStop on itself
10911100
eof_ = true;
10921101
}
10931102

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
'use strict';
2+
const common = require('../common');
3+
if (!common.hasCrypto)
4+
common.skip('missing crypto');
5+
const tls = require('tls');
6+
const fixtures = require('../common/fixtures');
7+
const https = require('https');
8+
const key = fixtures.readKey('agent1-key.pem');
9+
const cert = fixtures.readKey('agent1-cert.pem');
10+
const net = require('net');
11+
const { Duplex } = require('stream');
12+
13+
class CustomAgent extends https.Agent {
14+
createConnection(options, cb) {
15+
const realSocket = net.createConnection(options);
16+
const stream = new Duplex({
17+
emitClose: false,
18+
read(n) {
19+
(function retry() {
20+
const data = realSocket.read();
21+
if (data === null)
22+
return realSocket.once('readable', retry);
23+
stream.push(data);
24+
})();
25+
},
26+
write(chunk, enc, callback) {
27+
realSocket.write(chunk, enc, callback);
28+
},
29+
});
30+
realSocket.on('end', () => {
31+
stream.push(null);
32+
});
33+
34+
stream.on('end', common.mustCall());
35+
return tls.connect({ ...options, socket: stream });
36+
}
37+
}
38+
39+
const httpsServer = https.createServer({
40+
key,
41+
cert,
42+
}, (req, res) => {
43+
httpsServer.close();
44+
res.end('hello world!');
45+
});
46+
httpsServer.listen(0, 'localhost', () => {
47+
const agent = new CustomAgent();
48+
https.get({
49+
host: 'localhost',
50+
port: httpsServer.address().port,
51+
agent,
52+
headers: { Connection: 'close' },
53+
rejectUnauthorized: false
54+
}, (res) => {
55+
res.resume();
56+
});
57+
});

0 commit comments

Comments
 (0)