Spaces:
Paused
Paused
File size: 14,848 Bytes
8c741f6 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 | "use strict";
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;
|