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
I see these problems in the code:
150 Opening data channel for directory
-like response.LF
's, while the FTP specification mandates CRLF
's. See "bare linefeeds received in ASCII mode" warning when listing directory on my FTP server