diff --git a/package.json b/package.json index d4fcc1e..f8b0c99 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "lint-fix": "eslint --fix \"./src/**/*.ts\"", "dev": "npm run clean && tsc --watch", "tdd": "mocha --watch", + "prepare": "tsc", "buildOnly": "tsc" }, "repository": { diff --git a/src/FtpContext.ts b/src/FtpContext.ts index 70f0e79..b051770 100644 --- a/src/FtpContext.ts +++ b/src/FtpContext.ts @@ -1,7 +1,7 @@ import { Socket } from "net" import { ConnectionOptions as TLSConnectionOptions, TLSSocket } from "tls" -import { parseControlResponse } from "./parseControlResponse" import { StringEncoding } from "./StringEncoding" +import { parseControlResponse } from "./parseControlResponse" interface Task { /** Handles a response for a task. */ @@ -361,16 +361,33 @@ export class FTPContext { protected _setupDefaultErrorHandlers(socket: Socket, identifier: string) { socket.once("error", error => { error.message += ` (${identifier})` - this.closeWithError(error) + if(identifier == "control socket") { + this.closeWithError(error) + } else { + //only close data socket, not the control socket as well + this._closeSocket(socket) + this._passToHandler(error) + } }) socket.once("close", hadError => { if (hadError) { - this.closeWithError(new Error(`Socket closed due to transmission error (${identifier})`)) + if(identifier == "control socket") { + this.closeWithError(new Error(`Socket closed due to transmission error (${identifier})`)) + } else { + //only close data socket, not the control socket as well + this._closeSocket(socket) + this._passToHandler(new Error(`Socket closed due to transmission error (${identifier})`)) + } } }) socket.once("timeout", () => { socket.destroy() - this.closeWithError(new Error(`Timeout (${identifier})`)) + if(identifier == "control socket") { + this.closeWithError(new Error(`Timeout (${identifier})`)) + } else { + this._closeSocket(socket) + this._passToHandler(new Error(`Timeout (${identifier})`)) + } }) } diff --git a/test/MockFtpServer.js b/test/MockFtpServer.js index 541501e..dc1aaab 100644 --- a/test/MockFtpServer.js +++ b/test/MockFtpServer.js @@ -39,6 +39,15 @@ module.exports = class MockFtpServer { }) this.dataConn = undefined this.dataServer = net.createServer(conn => { + if(!conn.resetAndDestroy) { + conn.resetAndDestroy = () => { + conn._handle.reset(() => { + this.conn.emit("close") + }) + conn._handle.onread = () =>{}; + conn._handle = null; + } + } this.dataConn = conn this.connections.push(conn) this.didOpenDataConn() diff --git a/test/downloadSpec.js b/test/downloadSpec.js index a698ce8..fc6c67a 100644 --- a/test/downloadSpec.js +++ b/test/downloadSpec.js @@ -4,6 +4,7 @@ const { StringWriter } = require("../dist/StringWriter"); const MockFtpServer = require("./MockFtpServer"); const { Writable } = require("stream") const fs = require("fs"); +const { Socket } = require("net"); const FILENAME = "file.txt" const TIMEOUT = 1000 @@ -146,6 +147,28 @@ describe("Download to stream", function() { await this.client.downloadTo(buf, FILENAME) dataSocket.destroy(new Error("Error that should be ignored because task has completed successfully")) }) + it("handles early data socket closure", async () => { + /** + * type of this.client + * @type {Client} + */ + this.client; + + this.server.addHandlers({ + "pasv": () => `227 Entering Passive Mode (${this.server.dataAddressForPasvResponse})`, + "retr": ({arg}) => { + //close data connection such that client receives ECONNRESET + this.server.dataConn.resetAndDestroy() + + return `550 ${arg}: No such file or directory.` + } + }) + + const buf = new StringWriter() + await this.client.downloadTo(buf, FILENAME).catch(err => {}) + //control socket should still be open + assert(this.client.ftp.socket?.writable) + }) it("stops tracking timeout after failure") it("can get a directory listing")