2

I'm going to write from scratch an FTP server mainly to understand how client/socket FTP communication works and to try to develop some customized functionalities.

I have a doubts on how server treats the PASV command received from the client as when I try to instantiate a new port, the client is disconnecting.

This is the full PHP code on which I'm working on:

<?
//-- Server runs on port :2121 and (at the moment) accept any user with any password
$server = new Ftpd(2121);
class ftpd {
    private $clients = array();                 //Array of connected clients
    private $server = "";                       //Server connection handler
    private $listen_address = "";               //Listen Address
    private $listen_port = 0;                   //Listen Port
    private $min_pasv_port = 15000;             //Port range for PASSIVE connection
    private $max_pasv_port = 16000;
    private $eol = "\n";                        //EndOfLine
/* Show log on stdout */
    private function log($msg) {
        $output = date("d-M-Y H:i:s") . " - " . $msg;
        echo $output . "\n";
    }
/* Display socket error and abort */
    function socket_error($command = "") {
        $this->errorcode    = socket_last_error($this->server);
        $this->errormessage = socket_strerror($this->errorcode);
        $this->log("[ ERROR ] on command " . $command . "() : " . $this->errorcode . " - " . $this->errormessage);
        die();
    }
/* Get list of connections currently alive */
    private function socketlist() {
        $socketlist = array(
            'server' => $this->server
        );
        reset($this->clients);
        while (list($k,$c) = each($this->clients)) {
            $socketlist[$k] = $c['conn'];
        }
        return($socketlist);
    }
/* Add new client */
    private function add_client($conn) {
        $clientID = uniqid("client_");
        socket_getpeername($conn, $ip, $port);
        $this->clients[$clientID] = array(
            'conn'      => $conn,
            'ip'        => $ip,
            'hostname'  => gethostbyaddr($ip),
            'port'      => $port,
            'id'        => $clientID,
            'user'      => '',
            'password'  => ''
        );
        return($this->clients[$clientID]);
    }
/* Get connected client list */
    private function get_client($clientID) {
        reset($this->clients);
        while (list($id,$c) = each($this->clients)) {
            if ($c['conn'] == $clientID)    return($c);
        }
        return(false);
    }
/* Remove a connection with a client */
    private function remove_client($clientID) {
        reset($this->clients);
        while (list($k,$c) = each($this->clients)) {
            if ($c['conn'] == $clientID)    unset($this->clients[$k]);
        }
        return(true);
    }
/* Constructor */
    function ftpd($listen_port = 21) {
        $listen_address = gethostbyname($_SERVER['HOSTNAME']);
        /* Open socket */
        if (! ($server = @socket_create(AF_INET, SOCK_STREAM, 0)))          $this->socket_error('socket_create');
        else                                                                $this->log("[ DONE ] socket_create");
        /* reuse listening socket address */
        if (! @socket_setopt($server, SOL_SOCKET, SO_REUSEADDR, 1))         $this->socket_error('socket_setopt');
        else                                                                $this->log("[ DONE ] socket_setopt");
        /* set socket to non-blocking */
        if (! @socket_set_nonblock($server))                                $this->socket_error('socket_set_nonblock');
        else                                                                $this->log("[ DONE ] socket_set_nonblock");
        /* bind socket with address and port */
        if (! @socket_bind($server, $listen_address, $listen_port))         $this->socket_error('socket_bind');
        else                                                                $this->log("[ DONE ] socket_bind on " . $listen_address . ":" . $listen_port);
        /* start listening */
        if (! @socket_listen($server))                                      $this->socket_error('socket_listen');
        else                                                                $this->log("[ DONE ] socket_listen");
        $this->server           = $server;
        $this->listen_address   = $listen_address;
        $this->listen_port      = $listen_port;
        /* Loop waiting connections */
        while (true) {
            $this->log("[ WAIT ] Accept incoming connections (" . count($this->clients) . " clients currently connected)");
            $write = NULL;
            $exeption = NULL;
            /* Build list of active sockets */
            $slist = $this->socketlist();
            if (socket_select($slist, $write, $exeption, 1, 0) > 0) {
                foreach($slist as $sock) {
                    if ($sock == $this->server) {
                        /* accept a connection on server */
                        $this->log("New connection");
                        if (! ($conn = socket_accept($this->server))) {
                            $this->socket_error('socket_accept');
                        } else {
                            $lastclient = $this->add_client($conn);
                            $this->log("Client " . $lastclient['hostname'] . " (" . $lastclient['ip'] . ":" . $lastclient['port'] . ") connected");
                            $this->write($lastclient['conn'], 220, "Welcome!");
                        }
                    } else {
                        $this->log("ANOTHER MESSAGE");
                        $this->read($sock);
                    }
                }
            }
        }
    }
/* write data to socket connection */
    function write($clientID, $id, $message) {
        $connected_client = $this->get_client($clientID);
        $this->log("[ WRITE to " . $connected_client['hostname'] . " ] Message: " . $id . " " . $message);
        if (! (socket_write($clientID, $id . " " . $message . "\r\n"))) $this->socket_error('socket_write');
    }
/* receive data from socket connection */
    function read($clientID) {
        $connected_client = $this->get_client($clientID);
        $keyclient = $connected_client['id'];
        $this->log("[ READ from " . $connected_client['hostname'] . " ] Ready");
        //$this->log("Client " . $connected_client['hostname'] . " (" . $connected_client['ip'] . ":" . $connected_client['port'] . ") ready to write");
        if (($msg = @socket_read($clientID, 1024)) === false || $msg == '') {
            if ($msg != '') $this->socket_error('socket_read');
            $this->log("[ READ from " . $connected_client['hostname'] . " ] **** Message: " . $msg);
            $this->remove_client($clientID);
            $this->log("[ DISCONNECT ] " . $clientID);
        } else {
            $msg = trim($msg);
            $this->log("[ READ from " . $connected_client['hostname'] . " ] Message: " . $msg);
            list($cmd, $cmd_option) = explode(" ", $msg, 2);
            if ($cmd == "USER") { //-- USER command received
                //-- any user are allowed to login with any password
                $this->clients[$keyclient]['user'] = $cmd_option;
                $this->Write($clientID, 331, "Password required for " . $cmd_option);
            } elseif ($cmd == "PASS") { //-- PASS command received
                //-- any user are allowed to login with any password
                $this->clients[$keyclient]['password'] = $cmd_option;
                $this->Write($clientID, 230, "Welcome!");
            } elseif ($cmd == "PWD") { //-- PWD command received
                $this->Write($clientID, 257, "/ is the current directory");
            } elseif ($cmd == "TYPE") { //-- TYPE command received
                $this->eol = ($cmd_option == "A" ? "\r\n" : "\n");
                $this->Write($clientID, 200, "TYPE set to " . $cmd_option);
            } elseif ($cmd == "SYST") { //-- SYST command received
                $this->Write($clientID, 215, "UNIX Type: L8");
            } elseif ($cmd == "AUTH") { //-- AUTH command to be implemented 
                $this->Write($clientID, 500, $msg . " handled but not understood");
            } elseif ($cmd == "PASV") { //-- PASV command to be implemented 
                while (true) {              /* loop until a free port can be used */
                    $port = rand($this->min_pasv_port, $this->max_pasv_port);
                    if (! ($conn = @socket_create(AF_INET, SOCK_STREAM, 0)))    $this->socket_error('PASV.socket_create');
                    else                                                        $this->log("[ DONE ] PASV.socket_create");
                    /* reuse listening socket address */
                    if (! @socket_setopt($conn, SOL_SOCKET, SO_REUSEADDR, 1))   $this->socket_error('PASV.socket_setopt');
                    else                                                        $this->log("[ DONE ] PASV.socket_setopt");
                    /* set socket to non-blocking */
                    if (! @socket_set_nonblock($conn))                          $this->socket_error('PASV.socket_set_nonblock');
                    else                                                        $this->log("[ DONE ] PASV.socket_set_nonblock");
                    /* bind socket with address and port */
                    if (! @socket_bind($conn, $this->listen_address, $port))    $this->socket_error('PASV.socket_bind');
                    else                                                        $this->log("[ DONE ] PASV.socket_bind on " . $this->listen_address . ":" . $port);
                    /* start listening */
                    if (! @socket_listen($conn))                                $this->socket_error('PASV.socket_listen');
                    else                                                        $this->log("[ DONE ] PASV.socket_listen");
                    $this->clients[$keyclient]['conn'] = $conn;
                    $this->clients[$keyclient]['port'] = $port;
                    $p1 = $port >>  8;
                    $p2 = $port & 0xff;
                    $tmp = str_replace(".", ",", $this->listen_address);
                    $this->Write($clientID, 227, "Entering Passive Mode (" . $tmp . "," . $p1 . "," . $p2 . ").");
                    print_r($this->clients);
                    break;
                }
            } elseif ($cmd == "LIST") { //-- LIST command to be developped
                exec("ls /ews/tmp", $output);
                $this->Write($clientID, "", implode("\n", $output));
                $this->Write($clientID, 226, "Transfer complete");
            } else {
                $this->Write($clientID, 500, $msg . " unhandled");
            }
        }
    }
}
?>

This is the server log when daemon starts

[/ews/tmp]# ./ftp.server
20-May-2016 11:45:51 - [ DONE ] socket_create
20-May-2016 11:45:51 - [ DONE ] socket_setopt
20-May-2016 11:45:51 - [ DONE ] socket_set_nonblock
20-May-2016 11:45:51 - [ DONE ] socket_bind on 164.130.21.98:2121
20-May-2016 11:45:51 - [ DONE ] socket_listen
20-May-2016 11:45:51 - [ WAIT ] Accept incoming connections (0 clients currently connected)
20-May-2016 11:45:52 - [ WAIT ] Accept incoming connections (0 clients currently connected)
//--message repeated till when client connects
20-May-2016 11:46:06 - [ WAIT ] Accept incoming connections (0 clients currently connected)
20-May-2016 11:46:06 - New connection
20-May-2016 11:46:06 - Client ewsserver (164.130.21.98:45071) connected
20-May-2016 11:46:06 - [ WRITE to ewsserver ] Message: 220 Welcome!
20-May-2016 11:46:06 - [ WAIT ] Accept incoming connections (1 clients currently connected)
20-May-2016 11:46:07 - [ WAIT ] Accept incoming connections (1 clients currently connected)
20-May-2016 11:46:07 - ANOTHER MESSAGE
20-May-2016 11:46:07 - [ READ from ewsserver ] Ready
20-May-2016 11:46:07 - [ READ from ewsserver ] Message: USER dummy
20-May-2016 11:46:07 - [ WRITE to ewsserver ] Message: 331 Password required for dummy
20-May-2016 11:46:07 - [ WAIT ] Accept incoming connections (1 clients currently connected)
20-May-2016 11:46:08 - ANOTHER MESSAGE
20-May-2016 11:46:08 - [ READ from ewsserver ] Ready
20-May-2016 11:46:08 - [ READ from ewsserver ] Message: PASS dummy
20-May-2016 11:46:08 - [ WRITE to ewsserver ] Message: 230 Welcome!
20-May-2016 11:46:08 - [ WAIT ] Accept incoming connections (1 clients currently connected)
20-May-2016 11:46:08 - ANOTHER MESSAGE
20-May-2016 11:46:08 - [ READ from ewsserver ] Ready
20-May-2016 11:46:08 - [ READ from ewsserver ] Message: SYST
20-May-2016 11:46:08 - [ WRITE to ewsserver ] Message: 215 UNIX Type: L8
20-May-2016 11:46:08 - [ WAIT ] Accept incoming connections (1 clients currently connected)
20-May-2016 11:46:09 - [ WAIT ] Accept incoming connections (1 clients currently connected)
//-- client type the "dir" command and PASV command is received
20-May-2016 11:46:13 - ANOTHER MESSAGE
20-May-2016 11:46:13 - [ READ from ewsserver ] Ready
20-May-2016 11:46:13 - [ READ from ewsserver ] Message: PASV
20-May-2016 11:46:13 - [ DONE ] PASV.socket_create
20-May-2016 11:46:13 - [ DONE ] PASV.socket_setopt
20-May-2016 11:46:13 - [ DONE ] PASV.socket_set_nonblock
20-May-2016 11:46:13 - [ DONE ] PASV.socket_bind on 164.130.21.98:15469
20-May-2016 11:46:13 - [ DONE ] PASV.socket_listen
20-May-2016 11:46:13 - [ WRITE to  ] Message: 227 Entering Passive Mode (164,130,21,98,60,109).
Array
(
    [client_573edcde66f87] => Array
        (
            [conn] => Resource id #7
            [ip] => 164.130.21.98
            [hostname] => ewsserver
            [port] => 15469
            [id] => client_573edcde66f87
            [user] => vega
            [password] => vega
        )

)
20-May-2016 11:46:13 - [ WAIT ] Accept incoming connections (1 clients currently connected)
20-May-2016 11:46:13 - ANOTHER MESSAGE
20-May-2016 11:46:13 - [ READ from ewsserver ] Ready
20-May-2016 11:46:13 - [ READ from ewsserver ] **** Message:
//-- Server disconnect
20-May-2016 11:46:13 - [ DISCONNECT ] Resource id #7
20-May-2016 11:46:13 - [ WAIT ] Accept incoming connections (0 clients currently connected)
20-May-2016 11:46:14 - [ WAIT ] Accept incoming connections (0 clients currently connected)
20-May-2016 11:46:15 - [ WAIT ] Accept incoming connections (0 clients currently connected)
20-May-2016 11:46:16 - [ WAIT ] Accept incoming connections (0 clients currently connected)
20-May-2016 11:46:17 - [ WAIT ] Accept incoming connections (0 clients currently connected)

whereas this is the client side command prompt:

Status: Resolving address of host.name.st.com
Status: Connecting to xxx.xxx.21.98:2121...
Status: Connection established, waiting for welcome message...
Response:   220 Welcome!
Command:    AUTH TLS
Response:   500 AUTH TLS handled but not understood
Command:    AUTH SSL
Response:   500 AUTH SSL handled but not understood
Status: Insecure server, it does not support FTP over TLS.
Command:    USER dummy
Response:   331 Password required for dummy
Command:    PASS *****
Response:   230 Welcome!
Command:    SYST
Response:   215 UNIX Type: L8
Command:    FEAT
Response:   500 FEAT unhandled
Status: Server does not support non-ASCII characters.
Status: Logged in
Status: Retrieving directory listing...
Command:    PWD
Response:   257 / is the current directory
Command:    TYPE I
Response:   200 TYPE set to I
Command:    PASV
Response:   227 Entering Passive Mode (xxx,xxx,21,98,60,172).
Command:    LIST
Error:  Disconnected from server: ECONNABORTED - Connection aborted
Error:  Failed to retrieve directory listing
Status: Disconnected from server
Status: Resolving address of host.name.st.com
Status: Connecting to xxx.xxx.21.98:2121...
Status: Connection established, waiting for welcome message...
Response:   220 Welcome!
Command:    AUTH TLS
Response:   500 AUTH TLS handled but not understood
Command:    AUTH SSL
Response:   500 AUTH SSL handled but not understood
Status: Insecure server, it does not support FTP over TLS.
Command:    USER dummy
Response:   331 Password required for dummy
Command:    PASS *****
Response:   230 Welcome!
Status: Server does not support non-ASCII characters.
Status: Logged in
Status: Retrieving directory listing...
Command:    PWD
Response:   257 / is the current directory
Command:    TYPE I
Response:   200 TYPE set to I
Command:    PASV
Response:   227 Entering Passive Mode (xxx,xxx,21,98,60,251).
Command:    LIST
Martin Prikryl
  • 188,800
  • 56
  • 490
  • 992
Stefano Radaelli
  • 1,088
  • 2
  • 15
  • 35

1 Answers1

3

I see these problems in the code:

  • The immediate problem is that you try to read from the accepted data connection. But it is the client that is "downloading" the directory listing. So after you eventually timeout reading (because the client is rightly not sending anything), you abort the connection.
  • You do not confirm the accepting of the data connection with 150 Opening data channel for directory-like response.
  • You write the listing to the control connection, not to the data connection.
  • You terminate the lines in the listing using LF's, while the FTP specification mandates CRLF's. See "bare linefeeds received in ASCII mode" warning when listing directory on my FTP server
Community
  • 1
  • 1
Martin Prikryl
  • 188,800
  • 56
  • 490
  • 992