transfer.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. exports.downloadTo = exports.uploadFrom = exports.connectForPassiveTransfer = exports.parsePasvResponse = exports.enterPassiveModeIPv4 = exports.parseEpsvResponse = exports.enterPassiveModeIPv6 = void 0;
  4. const netUtils_1 = require("./netUtils");
  5. const stream_1 = require("stream");
  6. const tls_1 = require("tls");
  7. const parseControlResponse_1 = require("./parseControlResponse");
  8. /**
  9. * Prepare a data socket using passive mode over IPv6.
  10. */
  11. async function enterPassiveModeIPv6(ftp) {
  12. const res = await ftp.request("EPSV");
  13. const port = parseEpsvResponse(res.message);
  14. if (!port) {
  15. throw new Error("Can't parse EPSV response: " + res.message);
  16. }
  17. const controlHost = ftp.socket.remoteAddress;
  18. if (controlHost === undefined) {
  19. throw new Error("Control socket is disconnected, can't get remote address.");
  20. }
  21. await connectForPassiveTransfer(controlHost, port, ftp);
  22. return res;
  23. }
  24. exports.enterPassiveModeIPv6 = enterPassiveModeIPv6;
  25. /**
  26. * Parse an EPSV response. Returns only the port as in EPSV the host of the control connection is used.
  27. */
  28. function parseEpsvResponse(message) {
  29. // Get port from EPSV response, e.g. "229 Entering Extended Passive Mode (|||6446|)"
  30. // Some FTP Servers such as the one on IBM i (OS/400) use ! instead of | in their EPSV response.
  31. const groups = message.match(/[|!]{3}(.+)[|!]/);
  32. if (groups === null || groups[1] === undefined) {
  33. throw new Error(`Can't parse response to 'EPSV': ${message}`);
  34. }
  35. const port = parseInt(groups[1], 10);
  36. if (Number.isNaN(port)) {
  37. throw new Error(`Can't parse response to 'EPSV', port is not a number: ${message}`);
  38. }
  39. return port;
  40. }
  41. exports.parseEpsvResponse = parseEpsvResponse;
  42. /**
  43. * Prepare a data socket using passive mode over IPv4.
  44. */
  45. async function enterPassiveModeIPv4(ftp) {
  46. const res = await ftp.request("PASV");
  47. const target = parsePasvResponse(res.message);
  48. if (!target) {
  49. throw new Error("Can't parse PASV response: " + res.message);
  50. }
  51. // If the host in the PASV response has a local address while the control connection hasn't,
  52. // we assume a NAT issue and use the IP of the control connection as the target for the data connection.
  53. // We can't always perform this replacement because it's possible (although unlikely) that the FTP server
  54. // indeed uses a different host for data connections.
  55. const controlHost = ftp.socket.remoteAddress;
  56. if ((0, netUtils_1.ipIsPrivateV4Address)(target.host) && controlHost && !(0, netUtils_1.ipIsPrivateV4Address)(controlHost)) {
  57. target.host = controlHost;
  58. }
  59. await connectForPassiveTransfer(target.host, target.port, ftp);
  60. return res;
  61. }
  62. exports.enterPassiveModeIPv4 = enterPassiveModeIPv4;
  63. /**
  64. * Parse a PASV response.
  65. */
  66. function parsePasvResponse(message) {
  67. // Get host and port from PASV response, e.g. "227 Entering Passive Mode (192,168,1,100,10,229)"
  68. const groups = message.match(/([-\d]+,[-\d]+,[-\d]+,[-\d]+),([-\d]+),([-\d]+)/);
  69. if (groups === null || groups.length !== 4) {
  70. throw new Error(`Can't parse response to 'PASV': ${message}`);
  71. }
  72. return {
  73. host: groups[1].replace(/,/g, "."),
  74. port: (parseInt(groups[2], 10) & 255) * 256 + (parseInt(groups[3], 10) & 255)
  75. };
  76. }
  77. exports.parsePasvResponse = parsePasvResponse;
  78. function connectForPassiveTransfer(host, port, ftp) {
  79. return new Promise((resolve, reject) => {
  80. let socket = ftp._newSocket();
  81. const handleConnErr = function (err) {
  82. err.message = "Can't open data connection in passive mode: " + err.message;
  83. reject(err);
  84. };
  85. const handleTimeout = function () {
  86. socket.destroy();
  87. reject(new Error(`Timeout when trying to open data connection to ${host}:${port}`));
  88. };
  89. socket.setTimeout(ftp.timeout);
  90. socket.on("error", handleConnErr);
  91. socket.on("timeout", handleTimeout);
  92. socket.connect({ port, host, family: ftp.ipFamily }, () => {
  93. if (ftp.socket instanceof tls_1.TLSSocket) {
  94. socket = (0, tls_1.connect)(Object.assign({}, ftp.tlsOptions, {
  95. socket,
  96. // Reuse the TLS session negotiated earlier when the control connection
  97. // was upgraded. Servers expect this because it provides additional
  98. // security: If a completely new session would be negotiated, a hacker
  99. // could guess the port and connect to the new data connection before we do
  100. // by just starting his/her own TLS session.
  101. session: ftp.socket.getSession()
  102. }));
  103. // It's the responsibility of the transfer task to wait until the
  104. // TLS socket issued the event 'secureConnect'. We can't do this
  105. // here because some servers will start upgrading after the
  106. // specific transfer request has been made. List and download don't
  107. // have to wait for this event because the server sends whenever it
  108. // is ready. But for upload this has to be taken into account,
  109. // see the details in the upload() function below.
  110. }
  111. // Let the FTPContext listen to errors from now on, remove local handler.
  112. socket.removeListener("error", handleConnErr);
  113. socket.removeListener("timeout", handleTimeout);
  114. ftp.dataSocket = socket;
  115. resolve();
  116. });
  117. });
  118. }
  119. exports.connectForPassiveTransfer = connectForPassiveTransfer;
  120. /**
  121. * Helps resolving/rejecting transfers.
  122. *
  123. * This is used internally for all FTP transfers. For example when downloading, the server might confirm
  124. * with "226 Transfer complete" when in fact the download on the data connection has not finished
  125. * yet. With all transfers we make sure that a) the result arrived and b) has been confirmed by
  126. * e.g. the control connection. We just don't know in which order this will happen.
  127. */
  128. class TransferResolver {
  129. /**
  130. * Instantiate a TransferResolver
  131. */
  132. constructor(ftp, progress) {
  133. this.ftp = ftp;
  134. this.progress = progress;
  135. this.response = undefined;
  136. this.dataTransferDone = false;
  137. }
  138. /**
  139. * Mark the beginning of a transfer.
  140. *
  141. * @param name - Name of the transfer, usually the filename.
  142. * @param type - Type of transfer, usually "upload" or "download".
  143. */
  144. onDataStart(name, type) {
  145. // Let the data socket be in charge of tracking timeouts during transfer.
  146. // The control socket sits idle during this time anyway and might provoke
  147. // a timeout unnecessarily. The control connection will take care
  148. // of timeouts again once data transfer is complete or failed.
  149. if (this.ftp.dataSocket === undefined) {
  150. throw new Error("Data transfer should start but there is no data connection.");
  151. }
  152. this.ftp.socket.setTimeout(0);
  153. this.ftp.dataSocket.setTimeout(this.ftp.timeout);
  154. this.progress.start(this.ftp.dataSocket, name, type);
  155. }
  156. /**
  157. * The data connection has finished the transfer.
  158. */
  159. onDataDone(task) {
  160. this.progress.updateAndStop();
  161. // Hand-over timeout tracking back to the control connection. It's possible that
  162. // we don't receive the response over the control connection that the transfer is
  163. // done. In this case, we want to correctly associate the resulting timeout with
  164. // the control connection.
  165. this.ftp.socket.setTimeout(this.ftp.timeout);
  166. if (this.ftp.dataSocket) {
  167. this.ftp.dataSocket.setTimeout(0);
  168. }
  169. this.dataTransferDone = true;
  170. this.tryResolve(task);
  171. }
  172. /**
  173. * The control connection reports the transfer as finished.
  174. */
  175. onControlDone(task, response) {
  176. this.response = response;
  177. this.tryResolve(task);
  178. }
  179. /**
  180. * An error has been reported and the task should be rejected.
  181. */
  182. onError(task, err) {
  183. this.progress.updateAndStop();
  184. this.ftp.socket.setTimeout(this.ftp.timeout);
  185. this.ftp.dataSocket = undefined;
  186. task.reject(err);
  187. }
  188. /**
  189. * Control connection sent an unexpected request requiring a response from our part. We
  190. * can't provide that (because unknown) and have to close the contrext with an error because
  191. * the FTP server is now caught up in a state we can't resolve.
  192. */
  193. onUnexpectedRequest(response) {
  194. const err = new Error(`Unexpected FTP response is requesting an answer: ${response.message}`);
  195. this.ftp.closeWithError(err);
  196. }
  197. tryResolve(task) {
  198. // To resolve, we need both control and data connection to report that the transfer is done.
  199. const canResolve = this.dataTransferDone && this.response !== undefined;
  200. if (canResolve) {
  201. this.ftp.dataSocket = undefined;
  202. task.resolve(this.response);
  203. }
  204. }
  205. }
  206. function uploadFrom(source, config) {
  207. const resolver = new TransferResolver(config.ftp, config.tracker);
  208. const fullCommand = `${config.command} ${config.remotePath}`;
  209. return config.ftp.handle(fullCommand, (res, task) => {
  210. if (res instanceof Error) {
  211. resolver.onError(task, res);
  212. }
  213. else if (res.code === 150 || res.code === 125) { // Ready to upload
  214. const dataSocket = config.ftp.dataSocket;
  215. if (!dataSocket) {
  216. resolver.onError(task, new Error("Upload should begin but no data connection is available."));
  217. return;
  218. }
  219. // If we are using TLS, we have to wait until the dataSocket issued
  220. // 'secureConnect'. If this hasn't happened yet, getCipher() returns undefined.
  221. const canUpload = "getCipher" in dataSocket ? dataSocket.getCipher() !== undefined : true;
  222. onConditionOrEvent(canUpload, dataSocket, "secureConnect", () => {
  223. config.ftp.log(`Uploading to ${(0, netUtils_1.describeAddress)(dataSocket)} (${(0, netUtils_1.describeTLS)(dataSocket)})`);
  224. resolver.onDataStart(config.remotePath, config.type);
  225. (0, stream_1.pipeline)(source, dataSocket, err => {
  226. if (err) {
  227. resolver.onError(task, err);
  228. }
  229. else {
  230. resolver.onDataDone(task);
  231. }
  232. });
  233. });
  234. }
  235. else if ((0, parseControlResponse_1.positiveCompletion)(res.code)) { // Transfer complete
  236. resolver.onControlDone(task, res);
  237. }
  238. else if ((0, parseControlResponse_1.positiveIntermediate)(res.code)) {
  239. resolver.onUnexpectedRequest(res);
  240. }
  241. // Ignore all other positive preliminary response codes (< 200)
  242. });
  243. }
  244. exports.uploadFrom = uploadFrom;
  245. function downloadTo(destination, config) {
  246. if (!config.ftp.dataSocket) {
  247. throw new Error("Download will be initiated but no data connection is available.");
  248. }
  249. const resolver = new TransferResolver(config.ftp, config.tracker);
  250. return config.ftp.handle(config.command, (res, task) => {
  251. if (res instanceof Error) {
  252. resolver.onError(task, res);
  253. }
  254. else if (res.code === 150 || res.code === 125) { // Ready to download
  255. const dataSocket = config.ftp.dataSocket;
  256. if (!dataSocket) {
  257. resolver.onError(task, new Error("Download should begin but no data connection is available."));
  258. return;
  259. }
  260. config.ftp.log(`Downloading from ${(0, netUtils_1.describeAddress)(dataSocket)} (${(0, netUtils_1.describeTLS)(dataSocket)})`);
  261. resolver.onDataStart(config.remotePath, config.type);
  262. (0, stream_1.pipeline)(dataSocket, destination, err => {
  263. if (err) {
  264. resolver.onError(task, err);
  265. }
  266. else {
  267. resolver.onDataDone(task);
  268. }
  269. });
  270. }
  271. else if (res.code === 350) { // Restarting at startAt.
  272. config.ftp.send("RETR " + config.remotePath);
  273. }
  274. else if ((0, parseControlResponse_1.positiveCompletion)(res.code)) { // Transfer complete
  275. resolver.onControlDone(task, res);
  276. }
  277. else if ((0, parseControlResponse_1.positiveIntermediate)(res.code)) {
  278. resolver.onUnexpectedRequest(res);
  279. }
  280. // Ignore all other positive preliminary response codes (< 200)
  281. });
  282. }
  283. exports.downloadTo = downloadTo;
  284. /**
  285. * Calls a function immediately if a condition is met or subscribes to an event and calls
  286. * it once the event is emitted.
  287. *
  288. * @param condition The condition to test.
  289. * @param emitter The emitter to use if the condition is not met.
  290. * @param eventName The event to subscribe to if the condition is not met.
  291. * @param action The function to call.
  292. */
  293. function onConditionOrEvent(condition, emitter, eventName, action) {
  294. if (condition === true) {
  295. action();
  296. }
  297. else {
  298. emitter.once(eventName, () => action());
  299. }
  300. }