molex-ftp-client 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,176 @@
1
+ const net = require('net');
2
+
3
+ /**
4
+ * Handle FTP connection establishment and authentication
5
+ */
6
+ class FTPConnection {
7
+ constructor(client) {
8
+ this.client = client;
9
+ }
10
+
11
+ /**
12
+ * Connect to FTP server and authenticate
13
+ * @param {Object} options - Connection options
14
+ * @param {string} options.host - FTP server host
15
+ * @param {number} [options.port=21] - FTP server port
16
+ * @param {string} [options.user='anonymous'] - Username
17
+ * @param {string} [options.password='anonymous@'] - Password
18
+ * @returns {Promise<void>}
19
+ */
20
+ async connect({ host, port = 21, user = 'anonymous', password = 'anonymous@' }) {
21
+ this.client._debug(`Connecting to ${host}:${port} as ${user}`);
22
+
23
+ return new Promise((resolve, reject) => {
24
+ this.client.socket = net.createConnection({ host, port }, () => {
25
+ this.client.connected = true;
26
+ this.client._debug('TCP connection established');
27
+
28
+ if (this.client.keepAlive) {
29
+ this.client.socket.setKeepAlive(true, 10000);
30
+ }
31
+
32
+ this.client.emit('connected');
33
+ });
34
+
35
+ this.client.socket.setEncoding('utf8');
36
+ this.client.socket.on('data', async (data) => {
37
+ this.client.buffer += data;
38
+ const lines = this.client.buffer.split('\r\n');
39
+ this.client.buffer = lines.pop();
40
+
41
+ for (const line of lines) {
42
+ if (line) {
43
+ this.client._debug('<<<', line);
44
+ this.client.emit('response', line);
45
+ const code = parseInt(line.substring(0, 3));
46
+
47
+ // Handle initial connection
48
+ if (code === 220 && !this.client.authenticated) {
49
+ try {
50
+ this.client._debug('Authenticating...');
51
+ await this.sendCommand(`USER ${user}`);
52
+ await this.sendCommand(`PASS ${password}`);
53
+ this.client.authenticated = true;
54
+ this.client._debug('Authentication successful');
55
+ resolve();
56
+ } catch (err) {
57
+ reject(err);
58
+ }
59
+ }
60
+ }
61
+ }
62
+ });
63
+
64
+ this.client.socket.on('error', (err) => {
65
+ this.client.emit('error', err);
66
+ reject(err);
67
+ });
68
+
69
+ this.client.socket.on('close', () => {
70
+ this.client.connected = false;
71
+ this.client.authenticated = false;
72
+ this.client.emit('close');
73
+ });
74
+
75
+ setTimeout(() => reject(new Error('Connection timeout')), 10000);
76
+ });
77
+ }
78
+
79
+ /**
80
+ * Send FTP command and wait for response
81
+ * @param {string} command - FTP command
82
+ * @param {boolean} allowPreliminary - Allow 1xx preliminary responses
83
+ * @returns {Promise<Object>}
84
+ */
85
+ sendCommand(command, allowPreliminary = false) {
86
+ const { maskPassword } = require('./utils');
87
+
88
+ return new Promise((resolve, reject) => {
89
+ if (!this.client.connected) {
90
+ return reject(new Error('Not connected'));
91
+ }
92
+
93
+ this.client._commandCount++;
94
+ this.client._lastCommand = command;
95
+ const cmdToLog = maskPassword(command);
96
+ this.client._debug('>>>', cmdToLog);
97
+
98
+ const timeoutId = setTimeout(() => {
99
+ this.client.removeListener('response', responseHandler);
100
+ reject(new Error(`Command timeout: ${cmdToLog}`));
101
+ }, this.client.timeout);
102
+
103
+ const responseHandler = (line) => {
104
+ clearTimeout(timeoutId);
105
+ const code = parseInt(line.substring(0, 3));
106
+ const message = line.substring(4);
107
+
108
+ // Check if this is a complete response (not a multi-line response in progress)
109
+ if (line.charAt(3) === ' ') {
110
+ // 1xx = Preliminary positive reply (command okay, another command expected)
111
+ // 2xx = Positive completion reply
112
+ // 3xx = Positive intermediate reply (command okay, awaiting more info)
113
+ // 4xx/5xx = Negative replies (errors)
114
+
115
+ if (code >= 100 && code < 200 && allowPreliminary) {
116
+ // Don't remove listener, wait for final response
117
+ this.client._debug('Preliminary response, waiting for completion...');
118
+ return;
119
+ }
120
+
121
+ clearTimeout(timeoutId);
122
+ this.client.removeListener('response', responseHandler);
123
+
124
+ if (code >= 200 && code < 400) {
125
+ resolve({ code, message, raw: line });
126
+ } else {
127
+ this.client._debug(`Error response: ${code}`);
128
+ reject(new Error(`FTP Error ${code}: ${message}`));
129
+ }
130
+ }
131
+ };
132
+
133
+ this.client.on('response', responseHandler);
134
+ this.client.socket.write(command + '\r\n');
135
+ });
136
+ }
137
+
138
+ /**
139
+ * Enter passive mode and get data connection info
140
+ * @returns {Promise<Object>}
141
+ */
142
+ async enterPassiveMode() {
143
+ const response = await this.sendCommand('PASV');
144
+ const match = response.message.match(/\((\d+),(\d+),(\d+),(\d+),(\d+),(\d+)\)/);
145
+
146
+ if (!match) {
147
+ throw new Error('Failed to parse PASV response');
148
+ }
149
+
150
+ const host = `${match[1]}.${match[2]}.${match[3]}.${match[4]}`;
151
+ const port = parseInt(match[5]) * 256 + parseInt(match[6]);
152
+
153
+ return { host, port };
154
+ }
155
+
156
+ /**
157
+ * Close connection
158
+ * @returns {Promise<void>}
159
+ */
160
+ async close() {
161
+ if (this.client.connected) {
162
+ this.client._debug('Closing connection...');
163
+ try {
164
+ await this.sendCommand('QUIT');
165
+ } catch (err) {
166
+ this.client._debug('Error during QUIT:', err.message);
167
+ }
168
+ this.client.socket.end();
169
+ this.client.connected = false;
170
+ this.client.authenticated = false;
171
+ this.client._debug('Connection closed');
172
+ }
173
+ }
174
+ }
175
+
176
+ module.exports = FTPConnection;
package/lib/utils.js ADDED
@@ -0,0 +1,84 @@
1
+ /**
2
+ * FTP Command helpers
3
+ * Utilities for building and parsing FTP commands
4
+ */
5
+
6
+ /**
7
+ * Parse PASV response to extract host and port
8
+ * @param {string} message - PASV response message
9
+ * @returns {Object} - { host, port }
10
+ */
11
+ function parsePasvResponse(message) {
12
+ const match = message.match(/\((\d+),(\d+),(\d+),(\d+),(\d+),(\d+)\)/);
13
+
14
+ if (!match) {
15
+ throw new Error('Failed to parse PASV response');
16
+ }
17
+
18
+ const host = `${match[1]}.${match[2]}.${match[3]}.${match[4]}`;
19
+ const port = parseInt(match[5]) * 256 + parseInt(match[6]);
20
+
21
+ return { host, port };
22
+ }
23
+
24
+ /**
25
+ * Parse MDTM response to Date object
26
+ * @param {string} message - MDTM response message
27
+ * @returns {Date}
28
+ */
29
+ function parseMdtmResponse(message) {
30
+ const match = message.match(/(\d{14})/);
31
+ if (!match) {
32
+ throw new Error('Failed to parse MDTM response');
33
+ }
34
+
35
+ const str = match[1];
36
+ const year = parseInt(str.substring(0, 4));
37
+ const month = parseInt(str.substring(4, 6)) - 1;
38
+ const day = parseInt(str.substring(6, 8));
39
+ const hour = parseInt(str.substring(8, 10));
40
+ const minute = parseInt(str.substring(10, 12));
41
+ const second = parseInt(str.substring(12, 14));
42
+
43
+ return new Date(Date.UTC(year, month, day, hour, minute, second));
44
+ }
45
+
46
+ /**
47
+ * Normalize FTP path
48
+ * @param {string} path - Path to normalize
49
+ * @returns {string}
50
+ */
51
+ function normalizePath(path) {
52
+ return path.replace(/\\/g, '/').replace(/\/+/g, '/');
53
+ }
54
+
55
+ /**
56
+ * Get parent directory path
57
+ * @param {string} filePath - File path
58
+ * @returns {string}
59
+ */
60
+ function getParentDir(filePath) {
61
+ const normalized = normalizePath(filePath);
62
+ const lastSlash = normalized.lastIndexOf('/');
63
+ if (lastSlash <= 0) {
64
+ return '/';
65
+ }
66
+ return normalized.substring(0, lastSlash);
67
+ }
68
+
69
+ /**
70
+ * Mask password in command for logging
71
+ * @param {string} command - FTP command
72
+ * @returns {string}
73
+ */
74
+ function maskPassword(command) {
75
+ return command.startsWith('PASS ') ? 'PASS ********' : command;
76
+ }
77
+
78
+ module.exports = {
79
+ parsePasvResponse,
80
+ parseMdtmResponse,
81
+ normalizePath,
82
+ getParentDir,
83
+ maskPassword
84
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "molex-ftp-client",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "Lightweight FTP client using native Node.js TCP sockets (net module) with zero dependencies",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -22,12 +22,12 @@
22
22
  "license": "ISC",
23
23
  "repository": {
24
24
  "type": "git",
25
- "url": "https://github.com/molexworks/ftp-client"
25
+ "url": "https://github.com/tonywied17/molex-ftp-client"
26
26
  },
27
27
  "bugs": {
28
- "url": "https://github.com/molexworks/ftp-client/issues"
28
+ "url": "https://github.com/tonywied17/molex-ftp-client/issues"
29
29
  },
30
- "homepage": "https://github.com/molexworks/ftp-client#readme",
30
+ "homepage": "https://github.com/tonywied17/molex-ftp-client#readme",
31
31
  "engines": {
32
32
  "node": ">=12.0.0"
33
33
  }