Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"lint-fix": "eslint --fix \"./src/**/*.ts\"",
"dev": "npm run clean && tsc --watch",
"tdd": "mocha --watch",
"prepare": "tsc",
"buildOnly": "tsc"
},
"repository": {
Expand Down
25 changes: 21 additions & 4 deletions src/FtpContext.ts
Original file line number Diff line number Diff line change
@@ -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. */
Expand Down Expand Up @@ -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})`))
}
})
}

Expand Down
9 changes: 9 additions & 0 deletions test/MockFtpServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
23 changes: 23 additions & 0 deletions test/downloadSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand Down