Spaces:
Paused
Paused
| ; | |
| Object.defineProperty(exports, "__esModule", { value: true }); | |
| exports.FTPContext = exports.FTPError = void 0; | |
| const net_1 = require("net"); | |
| const parseControlResponse_1 = require("./parseControlResponse"); | |
| /** | |
| * Describes an FTP server error response including the FTP response code. | |
| */ | |
| class FTPError extends Error { | |
| constructor(res) { | |
| super(res.message); | |
| this.name = this.constructor.name; | |
| this.code = res.code; | |
| } | |
| } | |
| exports.FTPError = FTPError; | |
| function doNothing() { | |
| /** Do nothing */ | |
| } | |
| /** | |
| * FTPContext holds the control and data sockets of an FTP connection and provides a | |
| * simplified way to interact with an FTP server, handle responses, errors and timeouts. | |
| * | |
| * It doesn't implement or use any FTP commands. It's only a foundation to make writing an FTP | |
| * client as easy as possible. You won't usually instantiate this, but use `Client`. | |
| */ | |
| class FTPContext { | |
| /** | |
| * Instantiate an FTP context. | |
| * | |
| * @param timeout - Timeout in milliseconds to apply to control and data connections. Use 0 for no timeout. | |
| * @param encoding - Encoding to use for control connection. UTF-8 by default. Use "latin1" for older servers. | |
| */ | |
| constructor(timeout = 0, encoding = "utf8") { | |
| this.timeout = timeout; | |
| /** Debug-level logging of all socket communication. */ | |
| this.verbose = false; | |
| /** IP version to prefer (4: IPv4, 6: IPv6, undefined: automatic). */ | |
| this.ipFamily = undefined; | |
| /** Options for TLS connections. */ | |
| this.tlsOptions = {}; | |
| /** A multiline response might be received as multiple chunks. */ | |
| this._partialResponse = ""; | |
| this._encoding = encoding; | |
| // Help Typescript understand that we do indeed set _socket in the constructor but use the setter method to do so. | |
| this._socket = this.socket = this._newSocket(); | |
| this._dataSocket = undefined; | |
| } | |
| /** | |
| * Close the context. | |
| */ | |
| close() { | |
| // Internally, closing a context is always described with an error. If there is still a task running, it will | |
| // abort with an exception that the user closed the client during a task. If no task is running, no exception is | |
| // thrown but all newly submitted tasks after that will abort the exception that the client has been closed. | |
| // In addition the user will get a stack trace pointing to where exactly the client has been closed. So in any | |
| // case use _closingError to determine whether a context is closed. This also allows us to have a single code-path | |
| // for closing a context making the implementation easier. | |
| const message = this._task ? "User closed client during task" : "User closed client"; | |
| const err = new Error(message); | |
| this.closeWithError(err); | |
| } | |
| /** | |
| * Close the context with an error. | |
| */ | |
| closeWithError(err) { | |
| // If this context already has been closed, don't overwrite the reason. | |
| if (this._closingError) { | |
| return; | |
| } | |
| this._closingError = err; | |
| // Close the sockets but don't fully reset this context to preserve `this._closingError`. | |
| this._closeControlSocket(); | |
| this._closeSocket(this._dataSocket); | |
| // Give the user's task a chance to react, maybe cleanup resources. | |
| this._passToHandler(err); | |
| // The task might not have been rejected by the user after receiving the error. | |
| this._stopTrackingTask(); | |
| } | |
| /** | |
| * Returns true if this context has been closed or hasn't been connected yet. You can reopen it with `access`. | |
| */ | |
| get closed() { | |
| return this.socket.remoteAddress === undefined || this._closingError !== undefined; | |
| } | |
| /** | |
| * Reset this contex and all of its state. | |
| */ | |
| reset() { | |
| this.socket = this._newSocket(); | |
| } | |
| /** | |
| * Get the FTP control socket. | |
| */ | |
| get socket() { | |
| return this._socket; | |
| } | |
| /** | |
| * Set the socket for the control connection. This will only close the current control socket | |
| * if the new one is not an upgrade to the current one. | |
| */ | |
| set socket(socket) { | |
| // No data socket should be open in any case where the control socket is set or upgraded. | |
| this.dataSocket = undefined; | |
| // This being a reset, reset any other state apart from the socket. | |
| this.tlsOptions = {}; | |
| this._partialResponse = ""; | |
| if (this._socket) { | |
| const newSocketUpgradesExisting = socket.localPort === this._socket.localPort; | |
| if (newSocketUpgradesExisting) { | |
| this._removeSocketListeners(this.socket); | |
| } | |
| else { | |
| this._closeControlSocket(); | |
| } | |
| } | |
| if (socket) { | |
| // Setting a completely new control socket is in essence something like a reset. That's | |
| // why we also close any open data connection above. We can go one step further and reset | |
| // a possible closing error. That means that a closed FTPContext can be "reopened" by | |
| // setting a new control socket. | |
| this._closingError = undefined; | |
| // Don't set a timeout yet. Timeout for control sockets is only active during a task, see handle() below. | |
| socket.setTimeout(0); | |
| socket.setEncoding(this._encoding); | |
| socket.setKeepAlive(true); | |
| socket.on("data", data => this._onControlSocketData(data)); | |
| // Server sending a FIN packet is treated as an error. | |
| socket.on("end", () => this.closeWithError(new Error("Server sent FIN packet unexpectedly, closing connection."))); | |
| // Control being closed without error by server is treated as an error. | |
| socket.on("close", hadError => { if (!hadError) | |
| this.closeWithError(new Error("Server closed connection unexpectedly.")); }); | |
| this._setupDefaultErrorHandlers(socket, "control socket"); | |
| } | |
| this._socket = socket; | |
| } | |
| /** | |
| * Get the current FTP data connection if present. | |
| */ | |
| get dataSocket() { | |
| return this._dataSocket; | |
| } | |
| /** | |
| * Set the socket for the data connection. This will automatically close the former data socket. | |
| */ | |
| set dataSocket(socket) { | |
| this._closeSocket(this._dataSocket); | |
| if (socket) { | |
| // Don't set a timeout yet. Timeout data socket should be activated when data transmission starts | |
| // and timeout on control socket is deactivated. | |
| socket.setTimeout(0); | |
| this._setupDefaultErrorHandlers(socket, "data socket"); | |
| } | |
| this._dataSocket = socket; | |
| } | |
| /** | |
| * Get the currently used encoding. | |
| */ | |
| get encoding() { | |
| return this._encoding; | |
| } | |
| /** | |
| * Set the encoding used for the control socket. | |
| * | |
| * See https://nodejs.org/api/buffer.html#buffer_buffers_and_character_encodings for what encodings | |
| * are supported by Node. | |
| */ | |
| set encoding(encoding) { | |
| this._encoding = encoding; | |
| if (this.socket) { | |
| this.socket.setEncoding(encoding); | |
| } | |
| } | |
| /** | |
| * Send an FTP command without waiting for or handling the result. | |
| */ | |
| send(command) { | |
| const containsPassword = command.startsWith("PASS"); | |
| const message = containsPassword ? "> PASS ###" : `> ${command}`; | |
| this.log(message); | |
| this._socket.write(command + "\r\n", this.encoding); | |
| } | |
| /** | |
| * Send an FTP command and handle the first response. Use this if you have a simple | |
| * request-response situation. | |
| */ | |
| request(command) { | |
| return this.handle(command, (res, task) => { | |
| if (res instanceof Error) { | |
| task.reject(res); | |
| } | |
| else { | |
| task.resolve(res); | |
| } | |
| }); | |
| } | |
| /** | |
| * Send an FTP command and handle any response until you resolve/reject. Use this if you expect multiple responses | |
| * to a request. This returns a Promise that will hold whatever the response handler passed on when resolving/rejecting its task. | |
| */ | |
| handle(command, responseHandler) { | |
| if (this._task) { | |
| const err = new Error("User launched a task while another one is still running. Forgot to use 'await' or '.then()'?"); | |
| err.stack += `\nRunning task launched at: ${this._task.stack}`; | |
| this.closeWithError(err); | |
| // Don't return here, continue with returning the Promise that will then be rejected | |
| // because the context closed already. That way, users will receive an exception where | |
| // they called this method by mistake. | |
| } | |
| return new Promise((resolveTask, rejectTask) => { | |
| this._task = { | |
| stack: new Error().stack || "Unknown call stack", | |
| responseHandler, | |
| resolver: { | |
| resolve: arg => { | |
| this._stopTrackingTask(); | |
| resolveTask(arg); | |
| }, | |
| reject: err => { | |
| this._stopTrackingTask(); | |
| rejectTask(err); | |
| } | |
| } | |
| }; | |
| if (this._closingError) { | |
| // This client has been closed. Provide an error that describes this one as being caused | |
| // by `_closingError`, include stack traces for both. | |
| const err = new Error(`Client is closed because ${this._closingError.message}`); // Type 'Error' is not correctly defined, doesn't have 'code'. | |
| err.stack += `\nClosing reason: ${this._closingError.stack}`; | |
| err.code = this._closingError.code !== undefined ? this._closingError.code : "0"; | |
| this._passToHandler(err); | |
| return; | |
| } | |
| // Only track control socket timeout during the lifecycle of a task. This avoids timeouts on idle sockets, | |
| // the default socket behaviour which is not expected by most users. | |
| this.socket.setTimeout(this.timeout); | |
| if (command) { | |
| this.send(command); | |
| } | |
| }); | |
| } | |
| /** | |
| * Log message if set to be verbose. | |
| */ | |
| log(message) { | |
| if (this.verbose) { | |
| // tslint:disable-next-line no-console | |
| console.log(message); | |
| } | |
| } | |
| /** | |
| * Return true if the control socket is using TLS. This does not mean that a session | |
| * has already been negotiated. | |
| */ | |
| get hasTLS() { | |
| return "encrypted" in this._socket; | |
| } | |
| /** | |
| * Removes reference to current task and handler. This won't resolve or reject the task. | |
| * @protected | |
| */ | |
| _stopTrackingTask() { | |
| // Disable timeout on control socket if there is no task active. | |
| this.socket.setTimeout(0); | |
| this._task = undefined; | |
| } | |
| /** | |
| * Handle incoming data on the control socket. The chunk is going to be of type `string` | |
| * because we let `socket` handle encoding with `setEncoding`. | |
| * @protected | |
| */ | |
| _onControlSocketData(chunk) { | |
| this.log(`< ${chunk}`); | |
| // This chunk might complete an earlier partial response. | |
| const completeResponse = this._partialResponse + chunk; | |
| const parsed = (0, parseControlResponse_1.parseControlResponse)(completeResponse); | |
| // Remember any incomplete remainder. | |
| this._partialResponse = parsed.rest; | |
| // Each response group is passed along individually. | |
| for (const message of parsed.messages) { | |
| const code = parseInt(message.substr(0, 3), 10); | |
| const response = { code, message }; | |
| const err = code >= 400 ? new FTPError(response) : undefined; | |
| this._passToHandler(err ? err : response); | |
| } | |
| } | |
| /** | |
| * Send the current handler a response. This is usually a control socket response | |
| * or a socket event, like an error or timeout. | |
| * @protected | |
| */ | |
| _passToHandler(response) { | |
| if (this._task) { | |
| this._task.responseHandler(response, this._task.resolver); | |
| } | |
| // Errors other than FTPError always close the client. If there isn't an active task to handle the error, | |
| // the next one submitted will receive it using `_closingError`. | |
| // There is only one edge-case: If there is an FTPError while no task is active, the error will be dropped. | |
| // But that means that the user sent an FTP command with no intention of handling the result. So why should the | |
| // error be handled? Maybe log it at least? Debug logging will already do that and the client stays useable after | |
| // FTPError. So maybe no need to do anything here. | |
| } | |
| /** | |
| * Setup all error handlers for a socket. | |
| * @protected | |
| */ | |
| _setupDefaultErrorHandlers(socket, identifier) { | |
| socket.once("error", error => { | |
| error.message += ` (${identifier})`; | |
| this.closeWithError(error); | |
| }); | |
| socket.once("close", hadError => { | |
| if (hadError) { | |
| this.closeWithError(new Error(`Socket closed due to transmission error (${identifier})`)); | |
| } | |
| }); | |
| socket.once("timeout", () => { | |
| socket.destroy(); | |
| this.closeWithError(new Error(`Timeout (${identifier})`)); | |
| }); | |
| } | |
| /** | |
| * Close the control socket. Sends QUIT, then FIN, and ignores any response or error. | |
| */ | |
| _closeControlSocket() { | |
| this._removeSocketListeners(this._socket); | |
| this._socket.on("error", doNothing); | |
| this.send("QUIT"); | |
| this._closeSocket(this._socket); | |
| } | |
| /** | |
| * Close a socket, ignores any error. | |
| * @protected | |
| */ | |
| _closeSocket(socket) { | |
| if (socket) { | |
| this._removeSocketListeners(socket); | |
| socket.on("error", doNothing); | |
| socket.destroy(); | |
| } | |
| } | |
| /** | |
| * Remove all default listeners for socket. | |
| * @protected | |
| */ | |
| _removeSocketListeners(socket) { | |
| socket.removeAllListeners(); | |
| // Before Node.js 10.3.0, using `socket.removeAllListeners()` without any name did not work: https://github.com/nodejs/node/issues/20923. | |
| socket.removeAllListeners("timeout"); | |
| socket.removeAllListeners("data"); | |
| socket.removeAllListeners("end"); | |
| socket.removeAllListeners("error"); | |
| socket.removeAllListeners("close"); | |
| socket.removeAllListeners("connect"); | |
| } | |
| /** | |
| * Provide a new socket instance. | |
| * | |
| * Internal use only, replaced for unit tests. | |
| */ | |
| _newSocket() { | |
| return new net_1.Socket(); | |
| } | |
| } | |
| exports.FTPContext = FTPContext; | |