molex-ftp-client 1.0.1 → 1.2.1
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.
- package/CHANGELOG.md +61 -0
- package/README.md +86 -3
- package/index.js +11 -469
- package/lib/FTPClient.js +226 -0
- package/lib/commands.js +323 -0
- package/lib/connection.js +176 -0
- package/lib/utils.js +84 -0
- package/package.json +1 -1
|
@@ -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
|
+
};
|