Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Active FTP Client for Node.js

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.

like image 488
Josh C. Avatar asked Sep 04 '13 00:09

Josh C.


1 Answers

The flow for Active mode FTP transfers goes roughly like this:

  • Connection preamble (USER/PASS)
  • Establish a client local socket for data
  • Inform the server of that socket (PORT)
  • Tell the server to open the remote file for writing (STOR)
  • Start writing the data from the data socket established above (socket.write())
  • Close the stream from the client side (socket.end()) to end the file transfer
  • Tell the server you are done (QUIT)
  • Clean up any open sockets and servers on the client

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);
}
like image 92
jmuise Avatar answered Nov 15 '22 21:11

jmuise