Пользовательский PHP FTP-сервер: клиент отключается после отправки команды LIST

Я собираюсь написать с нуля FTP-сервер, главным образом, чтобы понять, как работает связь клиент / сокет FTP и попытаться разработать некоторые настраиваемые функции.

У меня есть сомнения по поводу того, как сервер относится к PASV Команда, полученная от клиента, например, когда я пытаюсь создать новый порт, клиент отключается.

Это полный код PHP, над которым я работаю:

<?
//-- 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");
}
}
}
}
?>

Это журнал сервера при запуске демона

[/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)

тогда как это командная строка на стороне клиента:

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

1

Решение

Я вижу эти проблемы в коде:

  • Ближайшая проблема что вы пытаетесь прочитать из принятого подключения к данным. Но именно клиент «загружает» список каталогов. Таким образом, после того, как вы в конечном итоге прочитаете тайм-аут (поскольку клиент по праву ничего не отправляет), вы прерываете соединение.
  • Вы не подтверждаете согласие на передачу данных с 150 Opening data channel for directory-подобный ответ.
  • Вы записываете список в управляющее соединение, а не в соединение для передачи данных.
  • Вы заканчиваете строки в списке, используя LFв то время как спецификации FTP мандаты CRLF«S. Увидеть "чистые переводы строк в режиме ASCII" предупреждение при перечислении каталога на моем FTP-сервере
1

Другие решения

Других решений пока нет …

По вопросам рекламы [email protected]