Skip to content

stream: regression since v12.16.3 with TLS sockets backed by non-net.Socket streams #35904

Open
@mscdex

Description

@mscdex
  • Version: v12.16.3+, v14.0.0+, v15.0.0+, master
  • Platform: n/a
  • Subsystem: stream

What steps will reproduce the bug?

The following code needs a private key and certificate for an HTTPS server.

const { readFileSync } = require('fs');
const tls = require('tls');
const https = require('https');
const net = require('net');
const { Duplex } = require('stream');

class CustomAgent extends https.Agent {
  constructor() {
    super();
  }
  createConnection(options, cb) {
    const realSocket = net.createConnection(options);
    const stream = new Duplex({
      emitClose: false,
      read(n) {
        (function retry() {
          const data = realSocket.read();
          if (data === null)
            return realSocket.once('readable', retry);
          stream.push(data);
        })();
      },
      write(chunk, enc, callback) {
        realSocket.write(chunk, enc, callback);
      },
    });
    realSocket.on('end', () => stream.push(null));

    stream.on('end', () => {
      console.log('stream end');
    }).on('close', () => {
      console.log('stream close');
    });

    return tls.connect({ ...options, socket: stream });
  }
}

const httpsServer = https.createServer({
  key: readFileSync('https_key.pem'),
  cert: readFileSync('https_cert.pem'),
}, (req, res) => {
  httpsServer.close();
  res.end('hello world!');
});
httpsServer.listen(0, 'localhost', () => {
  const agent = new CustomAgent();
  https.get({
    host: 'localhost',
    port: httpsServer.address().port,
    agent,
    headers: { Connection: 'close' },
    ca: readFileSync('https_cert.pem'),
  }, (res) => {
    res.resume();
  });
});

How often does it reproduce? Is there a required condition?

Every time.

What is the expected behavior?

"stream end" should be displayed

What do you see instead?

Nothing is displayed.

Additional information

This was originally discovered while adding tests for custom HTTP and HTTPS agents for the ssh2 module. I believe the key here is mainly using a custom stream for the socket passed to tls.connect(). However the regression only became evident once I started setting emitClose: false because my agent implementation was relying on the 'close' event being emitted to know when to close the underlying ssh connection and emitting 'close' is expected to be handled by ssh2 (because the protocol has an explicit close message separate from "EOF").

Bisecting reveals ed21d32 as the bad commit. What's happening is that when TLSSocket sees that the socket option is some custom object/stream and not a net.Socket instance, it wraps the stream with a JSStreamSocket. JSStreamSocket happens to have a readStop() implementation that simply pauses the socket. Since ed21d32 added a check for this method's existence, it now gets called when the TLS portion ends, which means the underlying/original stream now stays paused and will never emit 'end' like it used to.

Judging by the added code comments, it appears that this change was made to appease some error on Windows. I think the original error should be solved in a more compatible way.

/cc @ronag @addaleax @lpinca

Metadata

Metadata

Assignees

No one assigned

    Labels

    regressionIssues related to regressions.streamIssues and PRs related to the stream subsystem.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions