Spaces:
Paused
Paused
File size: 13,358 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 | "use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.downloadTo = exports.uploadFrom = exports.connectForPassiveTransfer = exports.parsePasvResponse = exports.enterPassiveModeIPv4 = exports.parseEpsvResponse = exports.enterPassiveModeIPv6 = void 0;
const netUtils_1 = require("./netUtils");
const stream_1 = require("stream");
const tls_1 = require("tls");
const parseControlResponse_1 = require("./parseControlResponse");
/**
* Prepare a data socket using passive mode over IPv6.
*/
async function enterPassiveModeIPv6(ftp) {
const res = await ftp.request("EPSV");
const port = parseEpsvResponse(res.message);
if (!port) {
throw new Error("Can't parse EPSV response: " + res.message);
}
const controlHost = ftp.socket.remoteAddress;
if (controlHost === undefined) {
throw new Error("Control socket is disconnected, can't get remote address.");
}
await connectForPassiveTransfer(controlHost, port, ftp);
return res;
}
exports.enterPassiveModeIPv6 = enterPassiveModeIPv6;
/**
* Parse an EPSV response. Returns only the port as in EPSV the host of the control connection is used.
*/
function parseEpsvResponse(message) {
// Get port from EPSV response, e.g. "229 Entering Extended Passive Mode (|||6446|)"
// Some FTP Servers such as the one on IBM i (OS/400) use ! instead of | in their EPSV response.
const groups = message.match(/[|!]{3}(.+)[|!]/);
if (groups === null || groups[1] === undefined) {
throw new Error(`Can't parse response to 'EPSV': ${message}`);
}
const port = parseInt(groups[1], 10);
if (Number.isNaN(port)) {
throw new Error(`Can't parse response to 'EPSV', port is not a number: ${message}`);
}
return port;
}
exports.parseEpsvResponse = parseEpsvResponse;
/**
* Prepare a data socket using passive mode over IPv4.
*/
async function enterPassiveModeIPv4(ftp) {
const res = await ftp.request("PASV");
const target = parsePasvResponse(res.message);
if (!target) {
throw new Error("Can't parse PASV response: " + res.message);
}
// If the host in the PASV response has a local address while the control connection hasn't,
// we assume a NAT issue and use the IP of the control connection as the target for the data connection.
// We can't always perform this replacement because it's possible (although unlikely) that the FTP server
// indeed uses a different host for data connections.
const controlHost = ftp.socket.remoteAddress;
if ((0, netUtils_1.ipIsPrivateV4Address)(target.host) && controlHost && !(0, netUtils_1.ipIsPrivateV4Address)(controlHost)) {
target.host = controlHost;
}
await connectForPassiveTransfer(target.host, target.port, ftp);
return res;
}
exports.enterPassiveModeIPv4 = enterPassiveModeIPv4;
/**
* Parse a PASV response.
*/
function parsePasvResponse(message) {
// Get host and port from PASV response, e.g. "227 Entering Passive Mode (192,168,1,100,10,229)"
const groups = message.match(/([-\d]+,[-\d]+,[-\d]+,[-\d]+),([-\d]+),([-\d]+)/);
if (groups === null || groups.length !== 4) {
throw new Error(`Can't parse response to 'PASV': ${message}`);
}
return {
host: groups[1].replace(/,/g, "."),
port: (parseInt(groups[2], 10) & 255) * 256 + (parseInt(groups[3], 10) & 255)
};
}
exports.parsePasvResponse = parsePasvResponse;
function connectForPassiveTransfer(host, port, ftp) {
return new Promise((resolve, reject) => {
let socket = ftp._newSocket();
const handleConnErr = function (err) {
err.message = "Can't open data connection in passive mode: " + err.message;
reject(err);
};
const handleTimeout = function () {
socket.destroy();
reject(new Error(`Timeout when trying to open data connection to ${host}:${port}`));
};
socket.setTimeout(ftp.timeout);
socket.on("error", handleConnErr);
socket.on("timeout", handleTimeout);
socket.connect({ port, host, family: ftp.ipFamily }, () => {
if (ftp.socket instanceof tls_1.TLSSocket) {
socket = (0, tls_1.connect)(Object.assign({}, ftp.tlsOptions, {
socket,
// Reuse the TLS session negotiated earlier when the control connection
// was upgraded. Servers expect this because it provides additional
// security: If a completely new session would be negotiated, a hacker
// could guess the port and connect to the new data connection before we do
// by just starting his/her own TLS session.
session: ftp.socket.getSession()
}));
// It's the responsibility of the transfer task to wait until the
// TLS socket issued the event 'secureConnect'. We can't do this
// here because some servers will start upgrading after the
// specific transfer request has been made. List and download don't
// have to wait for this event because the server sends whenever it
// is ready. But for upload this has to be taken into account,
// see the details in the upload() function below.
}
// Let the FTPContext listen to errors from now on, remove local handler.
socket.removeListener("error", handleConnErr);
socket.removeListener("timeout", handleTimeout);
ftp.dataSocket = socket;
resolve();
});
});
}
exports.connectForPassiveTransfer = connectForPassiveTransfer;
/**
* Helps resolving/rejecting transfers.
*
* This is used internally for all FTP transfers. For example when downloading, the server might confirm
* with "226 Transfer complete" when in fact the download on the data connection has not finished
* yet. With all transfers we make sure that a) the result arrived and b) has been confirmed by
* e.g. the control connection. We just don't know in which order this will happen.
*/
class TransferResolver {
/**
* Instantiate a TransferResolver
*/
constructor(ftp, progress) {
this.ftp = ftp;
this.progress = progress;
this.response = undefined;
this.dataTransferDone = false;
}
/**
* Mark the beginning of a transfer.
*
* @param name - Name of the transfer, usually the filename.
* @param type - Type of transfer, usually "upload" or "download".
*/
onDataStart(name, type) {
// Let the data socket be in charge of tracking timeouts during transfer.
// The control socket sits idle during this time anyway and might provoke
// a timeout unnecessarily. The control connection will take care
// of timeouts again once data transfer is complete or failed.
if (this.ftp.dataSocket === undefined) {
throw new Error("Data transfer should start but there is no data connection.");
}
this.ftp.socket.setTimeout(0);
this.ftp.dataSocket.setTimeout(this.ftp.timeout);
this.progress.start(this.ftp.dataSocket, name, type);
}
/**
* The data connection has finished the transfer.
*/
onDataDone(task) {
this.progress.updateAndStop();
// Hand-over timeout tracking back to the control connection. It's possible that
// we don't receive the response over the control connection that the transfer is
// done. In this case, we want to correctly associate the resulting timeout with
// the control connection.
this.ftp.socket.setTimeout(this.ftp.timeout);
if (this.ftp.dataSocket) {
this.ftp.dataSocket.setTimeout(0);
}
this.dataTransferDone = true;
this.tryResolve(task);
}
/**
* The control connection reports the transfer as finished.
*/
onControlDone(task, response) {
this.response = response;
this.tryResolve(task);
}
/**
* An error has been reported and the task should be rejected.
*/
onError(task, err) {
this.progress.updateAndStop();
this.ftp.socket.setTimeout(this.ftp.timeout);
this.ftp.dataSocket = undefined;
task.reject(err);
}
/**
* Control connection sent an unexpected request requiring a response from our part. We
* can't provide that (because unknown) and have to close the contrext with an error because
* the FTP server is now caught up in a state we can't resolve.
*/
onUnexpectedRequest(response) {
const err = new Error(`Unexpected FTP response is requesting an answer: ${response.message}`);
this.ftp.closeWithError(err);
}
tryResolve(task) {
// To resolve, we need both control and data connection to report that the transfer is done.
const canResolve = this.dataTransferDone && this.response !== undefined;
if (canResolve) {
this.ftp.dataSocket = undefined;
task.resolve(this.response);
}
}
}
function uploadFrom(source, config) {
const resolver = new TransferResolver(config.ftp, config.tracker);
const fullCommand = `${config.command} ${config.remotePath}`;
return config.ftp.handle(fullCommand, (res, task) => {
if (res instanceof Error) {
resolver.onError(task, res);
}
else if (res.code === 150 || res.code === 125) { // Ready to upload
const dataSocket = config.ftp.dataSocket;
if (!dataSocket) {
resolver.onError(task, new Error("Upload should begin but no data connection is available."));
return;
}
// If we are using TLS, we have to wait until the dataSocket issued
// 'secureConnect'. If this hasn't happened yet, getCipher() returns undefined.
const canUpload = "getCipher" in dataSocket ? dataSocket.getCipher() !== undefined : true;
onConditionOrEvent(canUpload, dataSocket, "secureConnect", () => {
config.ftp.log(`Uploading to ${(0, netUtils_1.describeAddress)(dataSocket)} (${(0, netUtils_1.describeTLS)(dataSocket)})`);
resolver.onDataStart(config.remotePath, config.type);
(0, stream_1.pipeline)(source, dataSocket, err => {
if (err) {
resolver.onError(task, err);
}
else {
resolver.onDataDone(task);
}
});
});
}
else if ((0, parseControlResponse_1.positiveCompletion)(res.code)) { // Transfer complete
resolver.onControlDone(task, res);
}
else if ((0, parseControlResponse_1.positiveIntermediate)(res.code)) {
resolver.onUnexpectedRequest(res);
}
// Ignore all other positive preliminary response codes (< 200)
});
}
exports.uploadFrom = uploadFrom;
function downloadTo(destination, config) {
if (!config.ftp.dataSocket) {
throw new Error("Download will be initiated but no data connection is available.");
}
const resolver = new TransferResolver(config.ftp, config.tracker);
return config.ftp.handle(config.command, (res, task) => {
if (res instanceof Error) {
resolver.onError(task, res);
}
else if (res.code === 150 || res.code === 125) { // Ready to download
const dataSocket = config.ftp.dataSocket;
if (!dataSocket) {
resolver.onError(task, new Error("Download should begin but no data connection is available."));
return;
}
config.ftp.log(`Downloading from ${(0, netUtils_1.describeAddress)(dataSocket)} (${(0, netUtils_1.describeTLS)(dataSocket)})`);
resolver.onDataStart(config.remotePath, config.type);
(0, stream_1.pipeline)(dataSocket, destination, err => {
if (err) {
resolver.onError(task, err);
}
else {
resolver.onDataDone(task);
}
});
}
else if (res.code === 350) { // Restarting at startAt.
config.ftp.send("RETR " + config.remotePath);
}
else if ((0, parseControlResponse_1.positiveCompletion)(res.code)) { // Transfer complete
resolver.onControlDone(task, res);
}
else if ((0, parseControlResponse_1.positiveIntermediate)(res.code)) {
resolver.onUnexpectedRequest(res);
}
// Ignore all other positive preliminary response codes (< 200)
});
}
exports.downloadTo = downloadTo;
/**
* Calls a function immediately if a condition is met or subscribes to an event and calls
* it once the event is emitted.
*
* @param condition The condition to test.
* @param emitter The emitter to use if the condition is not met.
* @param eventName The event to subscribe to if the condition is not met.
* @param action The function to call.
*/
function onConditionOrEvent(condition, emitter, eventName, action) {
if (condition === true) {
action();
}
else {
emitter.once(eventName, () => action());
}
}
|