FtpContext.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. exports.FTPContext = exports.FTPError = void 0;
  4. const net_1 = require("net");
  5. const parseControlResponse_1 = require("./parseControlResponse");
  6. /**
  7. * Describes an FTP server error response including the FTP response code.
  8. */
  9. class FTPError extends Error {
  10. constructor(res) {
  11. super(res.message);
  12. this.name = this.constructor.name;
  13. this.code = res.code;
  14. }
  15. }
  16. exports.FTPError = FTPError;
  17. function doNothing() {
  18. /** Do nothing */
  19. }
  20. /**
  21. * FTPContext holds the control and data sockets of an FTP connection and provides a
  22. * simplified way to interact with an FTP server, handle responses, errors and timeouts.
  23. *
  24. * It doesn't implement or use any FTP commands. It's only a foundation to make writing an FTP
  25. * client as easy as possible. You won't usually instantiate this, but use `Client`.
  26. */
  27. class FTPContext {
  28. /**
  29. * Instantiate an FTP context.
  30. *
  31. * @param timeout - Timeout in milliseconds to apply to control and data connections. Use 0 for no timeout.
  32. * @param encoding - Encoding to use for control connection. UTF-8 by default. Use "latin1" for older servers.
  33. */
  34. constructor(timeout = 0, encoding = "utf8") {
  35. this.timeout = timeout;
  36. /** Debug-level logging of all socket communication. */
  37. this.verbose = false;
  38. /** IP version to prefer (4: IPv4, 6: IPv6, undefined: automatic). */
  39. this.ipFamily = undefined;
  40. /** Options for TLS connections. */
  41. this.tlsOptions = {};
  42. /** A multiline response might be received as multiple chunks. */
  43. this._partialResponse = "";
  44. this._encoding = encoding;
  45. // Help Typescript understand that we do indeed set _socket in the constructor but use the setter method to do so.
  46. this._socket = this.socket = this._newSocket();
  47. this._dataSocket = undefined;
  48. }
  49. /**
  50. * Close the context.
  51. */
  52. close() {
  53. // Internally, closing a context is always described with an error. If there is still a task running, it will
  54. // abort with an exception that the user closed the client during a task. If no task is running, no exception is
  55. // thrown but all newly submitted tasks after that will abort the exception that the client has been closed.
  56. // In addition the user will get a stack trace pointing to where exactly the client has been closed. So in any
  57. // case use _closingError to determine whether a context is closed. This also allows us to have a single code-path
  58. // for closing a context making the implementation easier.
  59. const message = this._task ? "User closed client during task" : "User closed client";
  60. const err = new Error(message);
  61. this.closeWithError(err);
  62. }
  63. /**
  64. * Close the context with an error.
  65. */
  66. closeWithError(err) {
  67. // If this context already has been closed, don't overwrite the reason.
  68. if (this._closingError) {
  69. return;
  70. }
  71. this._closingError = err;
  72. // Close the sockets but don't fully reset this context to preserve `this._closingError`.
  73. this._closeControlSocket();
  74. this._closeSocket(this._dataSocket);
  75. // Give the user's task a chance to react, maybe cleanup resources.
  76. this._passToHandler(err);
  77. // The task might not have been rejected by the user after receiving the error.
  78. this._stopTrackingTask();
  79. }
  80. /**
  81. * Returns true if this context has been closed or hasn't been connected yet. You can reopen it with `access`.
  82. */
  83. get closed() {
  84. return this.socket.remoteAddress === undefined || this._closingError !== undefined;
  85. }
  86. /**
  87. * Reset this contex and all of its state.
  88. */
  89. reset() {
  90. this.socket = this._newSocket();
  91. }
  92. /**
  93. * Get the FTP control socket.
  94. */
  95. get socket() {
  96. return this._socket;
  97. }
  98. /**
  99. * Set the socket for the control connection. This will only close the current control socket
  100. * if the new one is not an upgrade to the current one.
  101. */
  102. set socket(socket) {
  103. // No data socket should be open in any case where the control socket is set or upgraded.
  104. this.dataSocket = undefined;
  105. // This being a reset, reset any other state apart from the socket.
  106. this.tlsOptions = {};
  107. this._partialResponse = "";
  108. if (this._socket) {
  109. const newSocketUpgradesExisting = socket.localPort === this._socket.localPort;
  110. if (newSocketUpgradesExisting) {
  111. this._removeSocketListeners(this.socket);
  112. }
  113. else {
  114. this._closeControlSocket();
  115. }
  116. }
  117. if (socket) {
  118. // Setting a completely new control socket is in essence something like a reset. That's
  119. // why we also close any open data connection above. We can go one step further and reset
  120. // a possible closing error. That means that a closed FTPContext can be "reopened" by
  121. // setting a new control socket.
  122. this._closingError = undefined;
  123. // Don't set a timeout yet. Timeout for control sockets is only active during a task, see handle() below.
  124. socket.setTimeout(0);
  125. socket.setEncoding(this._encoding);
  126. socket.setKeepAlive(true);
  127. socket.on("data", data => this._onControlSocketData(data));
  128. // Server sending a FIN packet is treated as an error.
  129. socket.on("end", () => this.closeWithError(new Error("Server sent FIN packet unexpectedly, closing connection.")));
  130. // Control being closed without error by server is treated as an error.
  131. socket.on("close", hadError => { if (!hadError)
  132. this.closeWithError(new Error("Server closed connection unexpectedly.")); });
  133. this._setupDefaultErrorHandlers(socket, "control socket");
  134. }
  135. this._socket = socket;
  136. }
  137. /**
  138. * Get the current FTP data connection if present.
  139. */
  140. get dataSocket() {
  141. return this._dataSocket;
  142. }
  143. /**
  144. * Set the socket for the data connection. This will automatically close the former data socket.
  145. */
  146. set dataSocket(socket) {
  147. this._closeSocket(this._dataSocket);
  148. if (socket) {
  149. // Don't set a timeout yet. Timeout data socket should be activated when data transmission starts
  150. // and timeout on control socket is deactivated.
  151. socket.setTimeout(0);
  152. this._setupDefaultErrorHandlers(socket, "data socket");
  153. }
  154. this._dataSocket = socket;
  155. }
  156. /**
  157. * Get the currently used encoding.
  158. */
  159. get encoding() {
  160. return this._encoding;
  161. }
  162. /**
  163. * Set the encoding used for the control socket.
  164. *
  165. * See https://nodejs.org/api/buffer.html#buffer_buffers_and_character_encodings for what encodings
  166. * are supported by Node.
  167. */
  168. set encoding(encoding) {
  169. this._encoding = encoding;
  170. if (this.socket) {
  171. this.socket.setEncoding(encoding);
  172. }
  173. }
  174. /**
  175. * Send an FTP command without waiting for or handling the result.
  176. */
  177. send(command) {
  178. const containsPassword = command.startsWith("PASS");
  179. const message = containsPassword ? "> PASS ###" : `> ${command}`;
  180. this.log(message);
  181. this._socket.write(command + "\r\n", this.encoding);
  182. }
  183. /**
  184. * Send an FTP command and handle the first response. Use this if you have a simple
  185. * request-response situation.
  186. */
  187. request(command) {
  188. return this.handle(command, (res, task) => {
  189. if (res instanceof Error) {
  190. task.reject(res);
  191. }
  192. else {
  193. task.resolve(res);
  194. }
  195. });
  196. }
  197. /**
  198. * Send an FTP command and handle any response until you resolve/reject. Use this if you expect multiple responses
  199. * to a request. This returns a Promise that will hold whatever the response handler passed on when resolving/rejecting its task.
  200. */
  201. handle(command, responseHandler) {
  202. if (this._task) {
  203. const err = new Error("User launched a task while another one is still running. Forgot to use 'await' or '.then()'?");
  204. err.stack += `\nRunning task launched at: ${this._task.stack}`;
  205. this.closeWithError(err);
  206. // Don't return here, continue with returning the Promise that will then be rejected
  207. // because the context closed already. That way, users will receive an exception where
  208. // they called this method by mistake.
  209. }
  210. return new Promise((resolveTask, rejectTask) => {
  211. this._task = {
  212. stack: new Error().stack || "Unknown call stack",
  213. responseHandler,
  214. resolver: {
  215. resolve: arg => {
  216. this._stopTrackingTask();
  217. resolveTask(arg);
  218. },
  219. reject: err => {
  220. this._stopTrackingTask();
  221. rejectTask(err);
  222. }
  223. }
  224. };
  225. if (this._closingError) {
  226. // This client has been closed. Provide an error that describes this one as being caused
  227. // by `_closingError`, include stack traces for both.
  228. const err = new Error(`Client is closed because ${this._closingError.message}`); // Type 'Error' is not correctly defined, doesn't have 'code'.
  229. err.stack += `\nClosing reason: ${this._closingError.stack}`;
  230. err.code = this._closingError.code !== undefined ? this._closingError.code : "0";
  231. this._passToHandler(err);
  232. return;
  233. }
  234. // Only track control socket timeout during the lifecycle of a task. This avoids timeouts on idle sockets,
  235. // the default socket behaviour which is not expected by most users.
  236. this.socket.setTimeout(this.timeout);
  237. if (command) {
  238. this.send(command);
  239. }
  240. });
  241. }
  242. /**
  243. * Log message if set to be verbose.
  244. */
  245. log(message) {
  246. if (this.verbose) {
  247. // tslint:disable-next-line no-console
  248. console.log(message);
  249. }
  250. }
  251. /**
  252. * Return true if the control socket is using TLS. This does not mean that a session
  253. * has already been negotiated.
  254. */
  255. get hasTLS() {
  256. return "encrypted" in this._socket;
  257. }
  258. /**
  259. * Removes reference to current task and handler. This won't resolve or reject the task.
  260. * @protected
  261. */
  262. _stopTrackingTask() {
  263. // Disable timeout on control socket if there is no task active.
  264. this.socket.setTimeout(0);
  265. this._task = undefined;
  266. }
  267. /**
  268. * Handle incoming data on the control socket. The chunk is going to be of type `string`
  269. * because we let `socket` handle encoding with `setEncoding`.
  270. * @protected
  271. */
  272. _onControlSocketData(chunk) {
  273. this.log(`< ${chunk}`);
  274. // This chunk might complete an earlier partial response.
  275. const completeResponse = this._partialResponse + chunk;
  276. const parsed = (0, parseControlResponse_1.parseControlResponse)(completeResponse);
  277. // Remember any incomplete remainder.
  278. this._partialResponse = parsed.rest;
  279. // Each response group is passed along individually.
  280. for (const message of parsed.messages) {
  281. const code = parseInt(message.substr(0, 3), 10);
  282. const response = { code, message };
  283. const err = code >= 400 ? new FTPError(response) : undefined;
  284. this._passToHandler(err ? err : response);
  285. }
  286. }
  287. /**
  288. * Send the current handler a response. This is usually a control socket response
  289. * or a socket event, like an error or timeout.
  290. * @protected
  291. */
  292. _passToHandler(response) {
  293. if (this._task) {
  294. this._task.responseHandler(response, this._task.resolver);
  295. }
  296. // Errors other than FTPError always close the client. If there isn't an active task to handle the error,
  297. // the next one submitted will receive it using `_closingError`.
  298. // There is only one edge-case: If there is an FTPError while no task is active, the error will be dropped.
  299. // But that means that the user sent an FTP command with no intention of handling the result. So why should the
  300. // error be handled? Maybe log it at least? Debug logging will already do that and the client stays useable after
  301. // FTPError. So maybe no need to do anything here.
  302. }
  303. /**
  304. * Setup all error handlers for a socket.
  305. * @protected
  306. */
  307. _setupDefaultErrorHandlers(socket, identifier) {
  308. socket.once("error", error => {
  309. error.message += ` (${identifier})`;
  310. this.closeWithError(error);
  311. });
  312. socket.once("close", hadError => {
  313. if (hadError) {
  314. this.closeWithError(new Error(`Socket closed due to transmission error (${identifier})`));
  315. }
  316. });
  317. socket.once("timeout", () => {
  318. socket.destroy();
  319. this.closeWithError(new Error(`Timeout (${identifier})`));
  320. });
  321. }
  322. /**
  323. * Close the control socket. Sends QUIT, then FIN, and ignores any response or error.
  324. */
  325. _closeControlSocket() {
  326. this._removeSocketListeners(this._socket);
  327. this._socket.on("error", doNothing);
  328. this.send("QUIT");
  329. this._closeSocket(this._socket);
  330. }
  331. /**
  332. * Close a socket, ignores any error.
  333. * @protected
  334. */
  335. _closeSocket(socket) {
  336. if (socket) {
  337. this._removeSocketListeners(socket);
  338. socket.on("error", doNothing);
  339. socket.destroy();
  340. }
  341. }
  342. /**
  343. * Remove all default listeners for socket.
  344. * @protected
  345. */
  346. _removeSocketListeners(socket) {
  347. socket.removeAllListeners();
  348. // Before Node.js 10.3.0, using `socket.removeAllListeners()` without any name did not work: https://github.com/nodejs/node/issues/20923.
  349. socket.removeAllListeners("timeout");
  350. socket.removeAllListeners("data");
  351. socket.removeAllListeners("end");
  352. socket.removeAllListeners("error");
  353. socket.removeAllListeners("close");
  354. socket.removeAllListeners("connect");
  355. }
  356. /**
  357. * Provide a new socket instance.
  358. *
  359. * Internal use only, replaced for unit tests.
  360. */
  361. _newSocket() {
  362. return new net_1.Socket();
  363. }
  364. }
  365. exports.FTPContext = FTPContext;