I'm trying my hand at writing an ftp client against Filezilla that supports active mode using node.js. I'm new to ftp and node.js. I thought I could get a good understanding of tcp socket communication and the ftp protocol by doing this exercise. Also, node-ftp an jsftp don't seem to support active mode, so I think this will be a nice (though rarely used) addition to npm.
I've got some proof of concept code that works at least sometimes, but not all the time. In the case where it works, the client uploads a file called file.txt
with the text 'hi'.
When it works, I get this:
220-FileZilla Server version 0.9.41 beta
220-written by Tim Kosse ([email protected])
220 Please visit http://sourceforge.net/projects/filezilla/
331 Password required for testuser
230 Logged on
listening
200 Port command successful
150 Opening data channel for file transfer.
server close
226 Transfer OK
half closed
closed
Process finished with exit code 0
When it doesn't work, I get this:
220-FileZilla Server version 0.9.41 beta
220-written by Tim Kosse ([email protected])
220 Please visit http://sourceforge.net/projects/filezilla/
331 Password required for testuser
230 Logged on
listening
200 Port command successful
150 Opening data channel for file transfer.
server close
half closed
closed
Process finished with exit code 0
So, I'm not getting the 226, and I'm not sure why I'm getting the inconsistent results.
Forgive the poorly written code. I'll refactor once I'm confident I understand how this should work.:
var net = require('net'),
Socket = net.Socket;
var cmdSocket = new Socket();
cmdSocket.setEncoding('binary')
var server = undefined;
var port = 21;
var host = "localhost";
var user = "testuser";
var password = "Password1*"
var active = true;
var supplyUser = true;
var supplyPassword = true;
var supplyPassive = true;
var waitingForCommand = true;
var sendFile = true;
function onConnect(){
}
var str="";
function onData(chunk) {
console.log(chunk.toString('binary'));
//if ftp server return code = 220
if(supplyUser){
supplyUser = false;
_send('USER ' + user, function(){
});
}else if(supplyPassword){
supplyPassword = false;
_send('PASS ' + password, function(){
});
}
else if(supplyPassive){
supplyPassive = false;
if(active){
server = net.createServer(function(socket){
console.log('new connection');
socket.setKeepAlive(true, 5000);
socket.write('hi', function(){
console.log('write done');
})
socket.on('connect', function(){
console.log('socket connect');
});
socket.on('data', function(d){
console.log('socket data: ' + d);
});
socket.on('error', function(err){
console.log('socket error: ' + err);
});
socket.on('end', function() {
console.log('socket end');
});
socket.on('drain', function(){
console.log('socket drain');
});
socket.on('timeout', function(){
console.log('socket timeout');
});
socket.on('close', function(){
console.log('socket close');
});
});
server.on('error', function(e){
console.log(e);
});
server.on('close', function(){
console.log('server close');
});
server.listen(function(){
console.log('listening');
var address = server.address();
var port = address.port;
var p1 = Math.floor(port/256);
var p2 = port % 256;
_sendCommand('PORT 127,0,0,1,' + p1 + ',' + p2, function(){
});
});
}else{
_send('PASV', function(){
});
}
}
else if(sendFile){
sendFile = false;
_send('STOR file.txt', function(){
});
}
else if(waitingForCommand){
waitingForCommand = false;
cmdSocket.end(null, function(){
});
if(server)server.close(function(){});
}
}
function onEnd() {
console.log('half closed');
}
function onClose(){
console.log('closed');
}
cmdSocket.once('connect', onConnect);
cmdSocket.on('data', onData);
cmdSocket.on('end', onEnd);
cmdSocket.on('close', onClose);
cmdSocket.connect(port, host);
function _send(cmd, callback){
cmdSocket.write(cmd + '\r\n', 'binary', callback);
}
Also, is the server
appropriate, or should I do it some other way?
EDIT: I changed the callback in server.listen to use a random port. This has removed the 425 I was getting previously. However, I am still not getting consistent behavior with the file transfer.
The flow for Active mode FTP transfers goes roughly like this:
USER
/PASS
)PORT
)STOR
)socket.write()
)socket.end()
) to end the file transferQUIT
)So once you've done this:
else if(sendFile){
sendFile = false;
_send('STOR file.txt', function(){
});
}
The server will respond with a 150
saying it has connected to the data socket you established and is ready to receive data.
One improvement to make it easier to reason about the execution at this point would be to change your control flow to operate on a parsed response code rather than pre-defined bools.
function onData(chunk) {
console.log(chunk);
var code = chunk.substring(0,3);
if(code == '220'){
instead of:
function onData(chunk) {
console.log(chunk.toString('binary'));
//if ftp server return code = 220
if(supplyUser){
Then you can add a section for sending the data:
//ready for data
else if (code == '150') {
dataSocket.write('some wonderful file contents\r\n', function(){});
dataSocket.end(null, function(){});
}
And a little more to clean up:
//transfer finished
else if ( code == '226') {
_send('QUIT', function(){ console.log("Saying Goodbye");});
}
//session end
else if ( code == '221') {
cmdSocket.end(null, function(){});
if(!!server){ server.close(); }
}
Obviously things will get more complicated if you are sending multiple files etc, but this should get your proof of concept running more reliably:
var net = require('net');
Socket = net.Socket;
var cmdSocket = new Socket();
cmdSocket.setEncoding('binary')
var server = undefined;
var dataSocket = undefined;
var port = 21;
var host = "localhost";
var user = "username";
var password = "password"
var active = true;
function onConnect(){
}
var str="";
function onData(chunk) {
console.log(chunk.toString('binary'));
var code = chunk.substring(0,3);
//if ftp server return code = 220
if(code == '220'){
_send('USER ' + user, function(){
});
}else if(code == '331'){
_send('PASS ' + password, function(){
});
}
else if(code == '230'){
if(active){
server = net.createServer(function(socket){
dataSocket = socket;
console.log('new connection');
socket.setKeepAlive(true, 5000);
socket.on('connect', function(){
console.log('socket connect');
});
socket.on('data', function(d){
console.log('socket data: ' + d);
});
socket.on('error', function(err){
console.log('socket error: ' + err);
});
socket.on('end', function() {
console.log('socket end');
});
socket.on('drain', function(){
console.log('socket drain');
});
socket.on('timeout', function(){
console.log('socket timeout');
});
socket.on('close', function(){
console.log('socket close');
});
});
server.on('error', function(e){
console.log(e);
});
server.on('close', function(){
console.log('server close');
});
server.listen(function(){
console.log('listening');
var address = server.address();
var port = address.port;
var p1 = Math.floor(port/256);
var p2 = port % 256;
_send('PORT 127,0,0,1,' + p1 + ',' + p2, function(){
});
});
}else{
_send('PASV', function(){
});
}
}
else if(code == '200'){
_send('STOR file.txt', function(){
});
}
//ready for data
else if (code == '150') {
dataSocket.write('some wonderful file contents\r\n', function(){});
dataSocket.end(null, function(){});
}
//transfer finished
else if ( code == '226') {
_send('QUIT', function(){ console.log("Saying Goodbye");});
}
//session end
else if ( code == '221') {
cmdSocket.end(null, function(){});
if(!!server){ server.close(); }
}
}
function onEnd() {
console.log('half closed');
}
function onClose(){
console.log('closed');
}
cmdSocket.once('connect', onConnect);
cmdSocket.on('data', onData);
cmdSocket.on('end', onEnd);
cmdSocket.on('close', onClose);
cmdSocket.connect(port, host);
function _send(cmd, callback){
cmdSocket.write(cmd + '\r\n', 'binary', callback);
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With