diff --git a/doc/api/net.md b/doc/api/net.md index 16a3a547fbb581..a08f23076a99a1 100644 --- a/doc/api/net.md +++ b/doc/api/net.md @@ -1089,6 +1089,20 @@ added: v0.5.10 The numeric representation of the remote port. For example, `80` or `21`. +### `socket.resetAndDestroy()` + + + +* Returns: {net.Socket} + +Close the TCP connection by sending an RST packet and destroy the stream. +If this TCP socket is in connecting status, it will send an RST packet and destroy this TCP socket once it is connected. +Otherwise, it will call `socket.destroy` this socket with an `ERR_SOCKET_CLOSED` Error. +If this is not a TCP socket (for example, a pipe), calling this method will immediately throw an `ERR_INVALID_HANDLE_TYPE` Error. +See [`writable.destroy()`][] for further details. + ### `socket.resume()` * Returns: {net.Socket} The socket itself. diff --git a/lib/net.js b/lib/net.js index aa8d317472c593..c49665c092c69f 100644 --- a/lib/net.js +++ b/lib/net.js @@ -642,12 +642,17 @@ Socket.prototype.end = function(data, encoding, callback) { }; Socket.prototype.resetAndDestroy = function() { - if (!(this._handle instanceof TCP)) - throw new ERR_INVALID_HANDLE_TYPE(); if (this._handle) { - debug('reset'); - this.resetAndClosing = true; - return this.destroy(); + if (!(this._handle instanceof TCP)) + throw new ERR_INVALID_HANDLE_TYPE(); + if (this.connecting) { + debug('reset wait for connection'); + this.once('connect', () => this._reset()); + } else { + this._reset(); + } + } else { + this.destroy(new ERR_SOCKET_CLOSED()); } return this; }; @@ -722,12 +727,13 @@ Socket.prototype._destroy = function(exception, cb) { this[kBytesWritten] = this._handle.bytesWritten; if (this.resetAndClosing) { + this.resetAndClosing = false; const err = this._handle.reset(() => { debug('emit close'); this.emit('close', isException); }); if (err) - throw errnoException(err, 'reset'); + this.emit('error', errnoException(err, 'reset')); } else { this._handle.close(() => { debug('emit close'); @@ -752,6 +758,12 @@ Socket.prototype._destroy = function(exception, cb) { } }; +Socket.prototype._reset = function() { + debug('reset connection'); + this.resetAndClosing = true; + return this.destroy(); +}; + Socket.prototype._getpeername = function() { if (!this._handle || !this._handle.getpeername) { return this._peername || {}; diff --git a/test/parallel/test-net-connect-reset-after-destroy.js b/test/parallel/test-net-connect-reset-after-destroy.js new file mode 100644 index 00000000000000..89e459229ab1bd --- /dev/null +++ b/test/parallel/test-net-connect-reset-after-destroy.js @@ -0,0 +1,29 @@ +'use strict'; +const common = require('../common'); +const net = require('net'); +const assert = require('assert'); + +const server = net.createServer(); +server.listen(0, common.mustCall(function() { + const port = server.address().port; + const conn = net.createConnection(port); + server.on('connection', (socket) => { + socket.on('error', common.expectsError({ + code: 'ECONNRESET', + message: 'read ECONNRESET', + name: 'Error' + })); + }); + + conn.on('connect', common.mustCall(function() { + assert.strictEqual(conn, conn.resetAndDestroy().destroy()); + conn.on('error', common.mustNotCall()); + + conn.write(Buffer.from('fzfzfzfzfz'), common.expectsError({ + code: 'ERR_STREAM_DESTROYED', + message: 'Cannot call write after a stream was destroyed', + name: 'Error' + })); + server.close(); + })); +})); diff --git a/test/parallel/test-net-connect-reset-before-connected.js b/test/parallel/test-net-connect-reset-before-connected.js new file mode 100644 index 00000000000000..1dc2b98183ce31 --- /dev/null +++ b/test/parallel/test-net-connect-reset-before-connected.js @@ -0,0 +1,13 @@ +'use strict'; +const common = require('../common'); +const net = require('net'); + +const server = net.createServer(); +server.listen(0); +const port = server.address().port; +const socket = net.connect(port, common.localhostIPv4, common.mustNotCall()); +socket.on('error', common.mustNotCall()); +server.close(); +socket.resetAndDestroy(); +// `reset` waiting socket connected to sent the RST packet +socket.destroy(); diff --git a/test/parallel/test-net-connect-reset-until-connected.js b/test/parallel/test-net-connect-reset-until-connected.js new file mode 100644 index 00000000000000..e40ec05f6ce1e9 --- /dev/null +++ b/test/parallel/test-net-connect-reset-until-connected.js @@ -0,0 +1,20 @@ +'use strict'; + +const common = require('../common'); +const net = require('net'); + +const server = net.createServer(); +server.listen(0, common.mustCall(function() { + const port = server.address().port; + const conn = net.createConnection(port); + conn.on('close', common.mustCall()); + server.on('connection', (socket) => { + socket.on('error', common.expectsError({ + code: 'ECONNRESET', + message: 'read ECONNRESET', + name: 'Error' + })); + server.close(); + }); + conn.resetAndDestroy(); +})); diff --git a/test/parallel/test-net-connect-reset.js b/test/parallel/test-net-connect-reset.js new file mode 100644 index 00000000000000..1f3e806aa99b74 --- /dev/null +++ b/test/parallel/test-net-connect-reset.js @@ -0,0 +1,13 @@ +'use strict'; +const common = require('../common'); +const net = require('net'); + +const socket = new net.Socket(); +socket.resetAndDestroy(); +// Emit error if socket is not connecting/connected +socket.on('error', common.mustCall( + common.expectsError({ + code: 'ERR_SOCKET_CLOSED', + name: 'Error' + })) +); diff --git a/test/parallel/test-net-server-reset.js b/test/parallel/test-net-server-reset.js new file mode 100644 index 00000000000000..ea78cd2743298e --- /dev/null +++ b/test/parallel/test-net-server-reset.js @@ -0,0 +1,36 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const net = require('net'); + +const sockets = []; + +const server = net.createServer(function(c) { + c.on('close', common.mustCall()); + + sockets.push(c); + + if (sockets.length === 2) { + assert.strictEqual(server.close(), server); + sockets.forEach((c) => c.resetAndDestroy()); + } +}); + +server.on('close', common.mustCall()); + +assert.strictEqual(server, server.listen(0, () => { + net.createConnection(server.address().port) + .on('error', common.mustCall( + common.expectsError({ + code: 'ECONNRESET', + name: 'Error' + })) + ); + net.createConnection(server.address().port) + .on('error', common.mustCall( + common.expectsError({ + code: 'ECONNRESET', + name: 'Error' + })) + ); +})); diff --git a/test/parallel/test-net-socket-reset-send.js b/test/parallel/test-net-socket-reset-send.js new file mode 100644 index 00000000000000..b7b9f66cb93d60 --- /dev/null +++ b/test/parallel/test-net-socket-reset-send.js @@ -0,0 +1,30 @@ +'use strict'; + +const common = require('../common'); +const net = require('net'); +const assert = require('assert'); + +const server = net.createServer(); +server.listen(0, common.mustCall(() => { + const port = server.address().port; + const conn = net.createConnection(port); + server.on('connection', (socket) => { + socket.on('error', common.expectsError({ + code: 'ECONNRESET', + message: 'read ECONNRESET', + name: 'Error' + })); + }); + + conn.on('connect', common.mustCall(() => { + assert.strictEqual(conn, conn.resetAndDestroy().destroy()); + conn.on('error', common.mustNotCall()); + + conn.write(Buffer.from('fzfzfzfzfz'), common.expectsError({ + code: 'ERR_STREAM_DESTROYED', + message: 'Cannot call write after a stream was destroyed', + name: 'Error' + })); + server.close(); + })); +})); diff --git a/test/parallel/test-net-socket-reset-twice.js b/test/parallel/test-net-socket-reset-twice.js new file mode 100644 index 00000000000000..0292c5e3ab5448 --- /dev/null +++ b/test/parallel/test-net-socket-reset-twice.js @@ -0,0 +1,15 @@ +'use strict'; +const common = require('../common'); +const net = require('net'); + +const server = net.createServer(); +server.listen(0); +const port = server.address().port; +const conn = net.createConnection(port); + +conn.on('error', common.mustCall(() => { + conn.resetAndDestroy(); +})); + +conn.on('close', common.mustCall()); +server.close();