2016-05-20 63 views
1

我將從頭開始編寫一個FTP服務器,主要是爲了瞭解客戶端/套接字FTP通信如何工作並嘗試開發一些定製功能。自定義PHP FTP服務器:發送LIST命令後客戶端斷開

我對服務器如何處理從客戶端收到的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

客戶端日誌文件?服務器日誌文件? –

+1

客戶端日誌文件會更好,我們不會看到客戶端發送了什麼命令以及它收到了什麼響應。 –

+0

目前的問題是您嘗試從接受的數據連接中讀取數據。但是它是客戶端正在「下載」目錄列表。所以,當你最終超時閱讀(因爲客戶端正確地不發送任何東西),你中止連接。看到我更新的答案。 –

回答

1

我看到這些問題在代碼:

  • 眼前的問題是您嘗試從接受的數據連接來讀取。但是它是客戶端正在「下載」目錄列表。所以,當你最終超時閱讀(因爲客戶端正確地不發送任何東西),你中止連接。
  • 您不確認接受與150 Opening data channel for directory類似的響應的數據連接。
  • 您將列表寫入控制連接,而不是數據連接。
  • 您使用LF來終止列表中的行,而FTP規範強制要求CRLF。請參閱"bare linefeeds received in ASCII mode" warning when listing directory on my FTP server
+0

這是正確的! PASV連接正常工作,但缺少'150目錄的開放數據通道'。 一旦實例化,我試圖發送文件的列表,但客戶端沒有收到它。我在哪裏可以找到在服務器端實現的LIST命令輸出的規範? –

+1

沒有規範。現代的FTP服務器和客戶端應該使用'MLSD'來代替,它有一個規範,[RFC 3659](https://tools.ietf.org/html/rfc3659)。 –

相關問題