淘小兔

  FTP服务器想必大家都不陌生,使用起来,现成的软件也很多。不过免费的软件功能有时候并不符合自己需求,又无法二次开发,付费软件价格又比较高昂。PHP的swoole扩展,是PHP语言的高性能网络通信框架,分享了PHP语言的异步多线程服务器,异步TCP/UDP网络客户端,异步MySQL,数据库连接池,AsyncTask,消息队列,毫秒定时器,异步文件读写,异步DNS查询。

  Swoole可以广泛应用于互联网、移动通信、企业软件、网络游戏、物联网、车联网、智能家庭等领域。 使用PHP+Swoole作为网络通信框架,可以使企业IT研发团队的效率大大提升,更加专注于开发创新产品。

  Swoole底层内置了异步非阻塞、多线程的网络IO服务器。PHP程序员仅需处理事件回调即可,无需关心底层。与Nginx/Tornado/Node.js等全异步的框架不同,Swoole既支持全异步,也支持同步。

 

  有了这个基础,基于TCP/IP协议的服务器开发就变得容易了。或许大家会问,C#等其他语言同样可以实现啊,为什么用PHP呢?我想主要考虑的还是开发效率。PHP是无需编译的脚本语言,开发部署速度快。

 

  无需赘言,这里列出步骤:

  00  准备平台,我这里使用的CentOS7;

  01  安装php和swoole扩展,请参考http://wiki.swoole.com/wiki/page/6.html

  02  设置字符集,由于ftp处理文件名容易出现乱码,建议将操作系统的字符集设置成GB18030,这样与Windows保持一致,目前大部分ftp客户端虽然也支持utf8文件名编码,但是,使用起来比不如意。如何好的解决方案请不吝告知,非常感谢;

  03  开始编写php程序,测试php程序;

  04  部署php版的ftp服务器。

 

  本文要实现ftp服务器的功能目标有:

  * 用户,组管理;  * 密码自助修改与重置;  * 文件夹权限管理;  * IP访问控制;  * 在线用户查看;  * 磁盘空间使用查看;  * SSL支持,保护密码及文件的传输安全;  * 内置web管理页面,方便进行远程管理。  项目目录:  FtpServer    |    +-conf    |   |    |  +-config.php  //FTP配置文件    |  +-ssl.crt    //ssl证书    |  +-ssl.key    //ssl密钥    |    +-inc    |  |    |  +-CSmtp.php     //smtp发邮件类,用于FTP密码发送和重置    |  +-ShareMemory.php //共享内存操作类    |  +-User.php     //用户管理、文件权限管理、IP访问控制    |    +-logs  //日志文件    |    +-reference //参考文档    |    +-web    |  |    |  +-wwwroot    //FTP Web管理网站    |  +-CWebServer.php //FTP内置http服务器    |    +-CFtpServer.php  //FTP服务器主程序    +-MyFtpServer.php  //FTP入口程序

 1.实现用户类CUser。

  用户的存储采用文本形式,将用户数组进行json编码。  

复制代码

用户文件格式: * array( *         'user1' => array( *             'pass'=>'', *             'group'=>'', *             'home'=>'/home/ftp/', //ftp主目录 *             'active'=>true, *             'expired=>'2015-12-12', *             'description'=>'', *             'email' => '', *             'folder'=>array( *                     //可以列出主目录下的文件和目录,但不能创建和删除,也不能进入主目录下的目录 *                     //前1-5位是文件权限,6-9是文件夹权限,10是否继承(inherit) *                     array('path'=>'/home/ftp/','access'=>'RWANDLCNDI'), *                     //可以列出/home/ftp/a/下的文件和目录,可以创建和删除,可以进入/home/ftp/a/下的子目录,可以创建和删除。 *                     array('path'=>'/home/ftp/a/','access'=>'RWAND-----'), *             ), *             'ip'=>array( *                 'allow'=>array(ip1,ip2,...),//支持*通配符: 192.168.0.* *                 'deny'=>array(ip1,ip2,...) *             ) *         )  * ) *  * 组文件格式: * array( *         'group1'=>array( *             'home'=>'/home/ftp/dept1/', *             'folder'=>array( *  *             ), *             'ip'=>array( *                 'allow'=>array(ip1,ip2,...), *                 'deny'=>array(ip1,ip2,...) *            ) *         ) * )

复制代码

 

  文件夹和文件的权限说明:

     

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

* 文件权限

* R读 : 允许用户读取(即下载)文件。该权限不允许用户列出目录内容,执行该操作需要列表权限。

* W写: 允许用户写入(即上传)文件。该权限不允许用户修改现有的文件,执行该操作需要追加权限。

* A追加: 允许用户向现有文件中追加数据。该权限通常用于使用户能够对部分上传的文件进行续传。

* N重命名: 允许用户重命名现有的文件。

* D删除: 允许用户删除文件。

*

* 目录权限

* L列表: 允许用户列出目录中包含的文件。

* C创建: 允许用户在目录中新建子目录。

* N重命名: 允许用户在目录中重命名现有子目录。

* D删除: 允许用户在目录中删除现有子目录。注意: 如果目录包含文件,用户要删除目录还需要具有删除文件权限。

*

* 子目录权限

* I继承: 允许所有子目录继承其父目录具有的相同权限。继承权限适用于大多数情况,但是如果访问必须受限于子文件夹,例如实施强制访问控制(Mandatory Access Control)时,则取消继承并为文件夹逐一授予权限。

*

  实现代码如下:  

  

用PHP实现一个高效安全的ftp服务器

复制代码

class User{        const I = 1;    // inherit        const FD = 2;    // folder delete        const FN = 4;    // folder rename        const FC = 8;    // folder create        const FL = 16;    // folder list        const D = 32;    // file delete        const N = 64;    // file rename        const A = 128;    // file append        const W = 256;    // file write (upload)        const R = 512;    // file read (download)            private $hash_salt = '';        private $user_file;        private $group_file;        private $users = array();        private $groups = array();        private $file_hash = '';         public function __construct(){        $this->user_file = BASE_PATH.'/conf/users';        $this->group_file = BASE_PATH.'/conf/groups';        $this->reload();    }        /**     * 返回权限表达式     * @param int $access     * @return string     */    public static function AC($access){        $str = '';        $char = array('R','W','A','N','D','L','C','N','D','I');        for($i = 0; $i < 10; $i++){            if($access & pow(2,9-$i))$str.= $char[$i];else $str.= '-';        }        return $str;    }        /**     * 加载用户数据     */    public function reload(){        $user_file_hash = md5_file($this->user_file);        $group_file_hash = md5_file($this->group_file);                if($this->file_hash != md5($user_file_hash.$group_file_hash)){            if(($user = file_get_contents($this->user_file)) !== false){                $this->users = json_decode($user,true);                if($this->users){                    //folder排序                    foreach ($this->users as $user=>$profile){                        if(isset($profile['folder'])){                            $this->users[$user]['folder'] = $this->sortFolder($profile['folder']);                        }                    }                }            }            if(($group = file_get_contents($this->group_file)) !== false){                $this->groups = json_decode($group,true);                if($this->groups){                    //folder排序                    foreach ($this->groups as $group=>$profile){                                                    if(isset($profile['folder'])){                                                    $this->groups[$group]['folder'] = $this->sortFolder($profile['folder']);                        }                    }                }            }            $this->file_hash = md5($user_file_hash.$group_file_hash);                    }    }        /**     * 对folder进行排序     * @return array     */    private function sortFolder($folder){        uasort($folder, function($a,$b){            return strnatcmp($a['path'], $b['path']);        });            $result = array();        foreach ($folder as $v){            $result[] = $v;        }            return $result;    }        /**     * 保存用户数据     */    public function save(){        file_put_contents($this->user_file, json_encode($this->users),LOCK_EX);        file_put_contents($this->group_file, json_encode($this->groups),LOCK_EX);    }        /**     * 添加用户     * @param string $user     * @param string $pass     * @param string $home     * @param string $expired     * @param boolean $active     * @param string $group     * @param string $description     * @param string $email     * @return boolean     */    public function addUser($user,$pass,$home,$expired,$active=true,$group='',$description='',$email = ''){        $user = strtolower($user);        if(isset($this->users[$user]) || empty($user)){            return false;        }                $this->users[$user] = array(                'pass' => md5($user.$this->hash_salt.$pass),                'home' => $home,                'expired' => $expired,                'active' => $active,                'group' => $group,                'description' => $description,                'email' => $email,        );        return true;    }        /**     * 设置用户资料     * @param string $user     * @param array $profile     * @return boolean     */    public function setUserProfile($user,$profile){        $user = strtolower($user);        if(is_array($profile) && isset($this->users[$user])){            if(isset($profile['pass'])){                $profile['pass'] = md5($user.$this->hash_salt.$profile['pass']);            }            if(isset($profile['active'])){                if(!is_bool($profile['active'])){                    $profile['active'] = $profile['active'] == 'true' ? true : false;                }            }                        $this->users[$user] = array_merge($this->users[$user],$profile);            return true;        }        return false;    }        /**     * 获取用户资料     * @param string $user     * @return multitype:|boolean     */    public function getUserProfile($user){        $user = strtolower($user);        if(isset($this->users[$user])){            return $this->users[$user];        }        return false;    }    /**     * 删除用户     * @param string $user     * @return boolean     */    public function delUser($user){        $user = strtolower($user);        if(isset($this->users[$user])){            unset($this->users[$user]);            return true;        }        return false;    }        /**     * 获取用户列表     * @return array     */    public function getUserList(){        $list = array();        if($this->users){            foreach ($this->users as $user=>$profile){                $list[] = $user;            }        }        sort($list);        return $list;    }        /**     * 添加组     * @param string $group     * @param string $home     * @return boolean     */    public function addGroup($group,$home){        $group = strtolower($group);        if(isset($this->groups[$group])){            return false;        }        $this->groups[$group] = array(                'home' => $home        );        return true;    }        /**     * 设置组资料     * @param string $group     * @param array $profile     * @return boolean     */    public function setGroupProfile($group,$profile){        $group = strtolower($group);        if(is_array($profile) && isset($this->groups[$group])){            $this->groups[$group] = array_merge($this->groups[$group],$profile);            return true;        }        return false;    }        /**     * 获取组资料     * @param string $group     * @return multitype:|boolean     */    public function getGroupProfile($group){        $group = strtolower($group);        if(isset($this->groups[$group])){            return $this->groups[$group];        }        return false;    }        /**     * 删除组     * @param string $group     * @return boolean     */    public function delGroup($group){        $group = strtolower($group);        if(isset($this->groups[$group])){            unset($this->groups[$group]);            foreach ($this->users as $user => $profile){                if($profile['group'] == $group)                    $this->users[$user]['group'] = '';            }            return true;        }        return false;    }        /**     * 获取组列表     * @return array     */    public function getGroupList(){        $list = array();        if($this->groups){            foreach ($this->groups as $group=>$profile){                $list[] = $group;            }        }        sort($list);        return $list;    }        /**     * 获取组用户列表     * @param string $group     * @return array     */    public function getUserListOfGroup($group){        $list = array();        if(isset($this->groups[$group]) && $this->users){            foreach ($this->users as $user=>$profile){                if(isset($profile['group']) && $profile['group'] == $group){                    $list[] = $user;                }            }        }        sort($list);        return $list;    }        /**     * 用户验证     * @param string $user     * @param string $pass     * @param string $ip     * @return boolean     */    public function checkUser($user,$pass,$ip = ''){        $this->reload();        $user = strtolower($user);        if(isset($this->users[$user])){            if($this->users[$user]['active'] && time() <= strtotime($this->users[$user]['expired'])                 && $this->users[$user]['pass'] == md5($user.$this->hash_salt.$pass)){                if(empty($ip)){                    return true;                }else{                    //ip验证                    return $this->checkIP($user, $ip);                }            }else{                return false;            }                }        return false;    }        /**     * basic auth      * @param string $base64         */    public function checkUserBasicAuth($base64){        $base64 = trim(str_replace('Basic ', '', $base64));        $str = base64_decode($base64);        if($str !== false){            list($user,$pass) = explode(':', $str,2);            $this->reload();            $user = strtolower($user);            if(isset($this->users[$user])){                $group = $this->users[$user]['group'];                if($group == 'admin' && $this->users[$user]['active'] && time() <= strtotime($this->users[$user]['expired'])                && $this->users[$user]['pass'] == md5($user.$this->hash_salt.$pass)){                                    return true;                }else{                    return false;                }            }        }        return false;    }        /**     * 用户登录ip验证     * @param string $user     * @param string $ip     *      * 用户的ip权限继承组的IP权限。     * 匹配规则:     * 1.进行组允许列表匹配;     * 2.如同通过,进行组拒绝列表匹配;     * 3.进行用户允许匹配     * 4.如果通过,进行用户拒绝匹配     *      */    public function checkIP($user,$ip){        $pass = false;        //先进行组验证                $group = $this->users[$user]['group'];        //组允许匹配        if(isset($this->groups[$group]['ip']['allow'])){            foreach ($this->groups[$group]['ip']['allow'] as $addr){                $pattern = '/'.str_replace('*','\d+',str_replace('.', '\.', $addr)).'/';                if(preg_match($pattern, $ip) && !empty($addr)){                    $pass = true;                    break;                }            }        }        //如果允许通过,进行拒绝匹配        if($pass){            if(isset($this->groups[$group]['ip']['deny'])){                foreach ($this->groups[$group]['ip']['deny'] as $addr){                    $pattern = '/'.str_replace('*','\d+',str_replace('.', '\.', $addr)).'/';                    if(preg_match($pattern, $ip) && !empty($addr)){                        $pass = false;                        break;                    }                }            }        }                if(isset($this->users[$user]['ip']['allow'])){                        foreach ($this->users[$user]['ip']['allow'] as $addr){                $pattern = '/'.str_replace('*','\d+',str_replace('.', '\.', $addr)).'/';                if(preg_match($pattern, $ip) && !empty($addr)){                    $pass = true;                    break;                }            }        }        if($pass){            if(isset($this->users[$user]['ip']['deny'])){                foreach ($this->users[$user]['ip']['deny'] as $addr){                    $pattern = '/'.str_replace('*','\d+',str_replace('.', '\.', $addr)).'/';                    if(preg_match($pattern, $ip) && !empty($addr)){                        $pass = false;                        break;                    }                }            }        }        echo date('Y-m-d H:i:s')." [debug]\tIP ACCESS:".' '.($pass?'true':'false')."\n";        return $pass;    }        /**     * 获取用户主目录     * @param string $user     * @return string     */    public function getHomeDir($user){        $user = strtolower($user);        $group = $this->users[$user]['group'];        $dir = '';        if($group){            if(isset($this->groups[$group]['home']))$dir = $this->groups[$group]['home'];        }        $dir = !empty($this->users[$user]['home'])?$this->users[$user]['home']:$dir;        return $dir;    }        //文件权限判断    public function isReadable($user,$path){                $result = $this->getPathAccess($user, $path);        if($result['isExactMatch']){            return $result['access'][0] == 'R';        }else{            return $result['access'][0] == 'R' && $result['access'][9] == 'I';        }    }            public function isWritable($user,$path){                $result = $this->getPathAccess($user, $path);                if($result['isExactMatch']){            return $result['access'][1] == 'W';        }else{            return $result['access'][1] == 'W' && $result['access'][9] == 'I';        }    }        public function isAppendable($user,$path){        $result = $this->getPathAccess($user, $path);        if($result['isExactMatch']){            return $result['access'][2] == 'A';        }else{            return $result['access'][2] == 'A' && $result['access'][9] == 'I';        }    }            public function isRenamable($user,$path){        $result = $this->getPathAccess($user, $path);        if($result['isExactMatch']){            return $result['access'][3] == 'N';        }else{            return $result['access'][3] == 'N' && $result['access'][9] == 'I';        }    }    public function isDeletable($user,$path){                $result = $this->getPathAccess($user, $path);        if($result['isExactMatch']){            return $result['access'][4] == 'D';        }else{            return $result['access'][4] == 'D' && $result['access'][9] == 'I';        }    }        //目录权限判断    public function isFolderListable($user,$path){        $result = $this->getPathAccess($user, $path);        if($result['isExactMatch']){            return $result['access'][5] == 'L';        }else{            return $result['access'][5] == 'L' && $result['access'][9] == 'I';        }    }        public function isFolderCreatable($user,$path){        $result = $this->getPathAccess($user, $path);        if($result['isExactMatch']){            return  $result['access'][6] == 'C';        }else{            return  $result['access'][6] == 'C' && $result['access'][9] == 'I';        }    }        public function isFolderRenamable($user,$path){        $result = $this->getPathAccess($user, $path);        if($result['isExactMatch']){            return $result['access'][7] == 'N';        }else{            return $result['access'][7] == 'N' && $result['access'][9] == 'I';        }    }        public function isFolderDeletable($user,$path){        $result = $this->getPathAccess($user, $path);        if($result['isExactMatch']){            return $result['access'][8] == 'D';        }else{            return $result['access'][8] == 'D' && $result['access'][9] == 'I';        }    }        /**     * 获取目录权限     * @param string $user     * @param string $path     * @return array     * 进行最长路径匹配     *      * 返回:     * array(     * 'access'=>目前权限         *    ,'isExactMatch'=>是否精确匹配     *         * );     *      * 如果精确匹配,则忽略inherit.     * 否则应判断是否继承父目录的权限,     * 权限位表:     * +---+---+---+---+---+---+---+---+---+---+     * | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |     * +---+---+---+---+---+---+---+---+---+---+     * | R | W | A | N | D | L | C | N | D | I |     * +---+---+---+---+---+---+---+---+---+---+     * |       FILE        |     FOLDER        |     * +-------------------+-------------------+     */        public function getPathAccess($user,$path){        $this->reload();        $user = strtolower($user);        $group = $this->users[$user]['group'];                //去除文件名称        $path = str_replace(substr(strrchr($path, '/'),1),'',$path);        $access = self::AC(0);                $isExactMatch = false;        if($group){            if(isset($this->groups[$group]['folder'])){                                foreach ($this->groups[$group]['folder'] as $f){                    //中文处理                    $t_path = iconv('UTF-8','GB18030',$f['path']);                                        if(strpos($path, $t_path) === 0){                        $access = $f['access'];                                                $isExactMatch = ($path == $t_path?true:false);                    }                                        }            }        }        if(isset($this->users[$user]['folder'])){            foreach ($this->users[$user]['folder'] as $f){                //中文处理                $t_path = iconv('UTF-8','GB18030',$f['path']);                if(strpos($path, $t_path) === 0){                    $access = $f['access'];                                        $isExactMatch = ($path == $t_path?true:false);                }            }        }        echo date('Y-m-d H:i:s')." [debug]\tACCESS:$access ".' '.($isExactMatch?'1':'0')." $path\n";        return array('access'=>$access,'isExactMatch'=>$isExactMatch);    }            /**     * 添加在线用户     * @param ShareMemory $shm     * @param swoole_server $serv     * @param unknown $user     * @param unknown $fd     * @param unknown $ip     * @return Ambigous <multitype:, boolean, mixed, multitype:unknown number multitype:Ambigous <unknown, number>  >     */    public function addOnline(ShareMemory $shm ,$serv,$user,$fd,$ip){        $shm_data = $shm->read();        if($shm_data !== false){            $shm_data['online'][$user.'-'.$fd] = array('ip'=>$ip,'time'=>time());            $shm_data['last_login'][] = array('user' => $user,'ip'=>$ip,'time'=>time());            //清除旧数据            if(count($shm_data['last_login'])>30)array_shift($shm_data['last_login']);            $list = array();            foreach ($shm_data['online'] as $k =>$v){                $arr = explode('-', $k);                if($serv->connection_info($arr[1]) !== false){                    $list[$k] = $v;                }            }            $shm_data['online'] = $list;            $shm->write($shm_data);        }        return $shm_data;    }        /**     * 添加登陆失败记录     * @param ShareMemory $shm     * @param unknown $user     * @param unknown $ip     * @return Ambigous <number, multitype:, boolean, mixed>     */    public function addAttempt(ShareMemory $shm ,$user,$ip){        $shm_data = $shm->read();        if($shm_data !== false){            if(isset($shm_data['login_attempt'][$ip.'||'.$user]['count'])){                $shm_data['login_attempt'][$ip.'||'.$user]['count'] += 1;            }else{                $shm_data['login_attempt'][$ip.'||'.$user]['count'] = 1;            }            $shm_data['login_attempt'][$ip.'||'.$user]['time'] = time();            //清除旧数据            if(count($shm_data['login_attempt'])>30)array_shift($shm_data['login_attempt']);            $shm->write($shm_data);        }        return $shm_data;    }        /**     * 密码错误上限     * @param unknown $shm     * @param unknown $user     * @param unknown $ip     * @return boolean     */    public function isAttemptLimit(ShareMemory $shm,$user,$ip){        $shm_data = $shm->read();        if($shm_data !== false){            if(isset($shm_data['login_attempt'][$ip.'||'.$user]['count'])){                if($shm_data['login_attempt'][$ip.'||'.$user]['count'] > 10 &&                time() - $shm_data['login_attempt'][$ip.'||'.$user]['time'] < 600){                                        return true;                }            }        }        return false;    }        /**     * 生成随机密钥     * @param int $len     * @return Ambigous <NULL, string>     */    public static function genPassword($len){        $str = null;        $strPol = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz@!#$%*+-";        $max = strlen($strPol)-1;            for($i=0;$i<$len;$i++){            $str.=$strPol[rand(0,$max)];//rand($min,$max)生成介于min和max两个数之间的一个随机整数        }        return $str;    }            }

复制代码

 

2.共享内存操作类

  这个相对简单,使用php的shmop扩展即可。

  

用PHP实现一个高效安全的ftp服务器

复制代码

class ShareMemory{        private $mode = 0644;        private $shm_key;        private $shm_size;        /**     * 构造函数          */    public function __construct(){        $key = 'F';        $size = 1024*1024;        $this->shm_key = ftok(__FILE__,$key);        $this->shm_size = $size + 1;    }        /**     * 读取内存数组     * @return array|boolean     */    public function read(){        if(($shm_id = shmop_open($this->shm_key,'c',$this->mode,$this->shm_size)) !== false){            $str = shmop_read($shm_id,1,$this->shm_size-1);            shmop_close($shm_id);            if(($i = strpos($str,"\0")) !== false)$str = substr($str,0,$i);            if($str){                return json_decode($str,true);            }else{                return array();            }        }        return false;    }        /**     * 写入数组到内存     * @param array $arr     * @return int|boolean     */    public function write($arr){        if(!is_array($arr))return false;        $str = json_encode($arr)."\0";        if(strlen($str) > $this->shm_size) return false;        if(($shm_id = shmop_open($this->shm_key,'c',$this->mode,$this->shm_size)) !== false){                        $count = shmop_write($shm_id,$str,1);            shmop_close($shm_id);            return $count;        }        return false;    }        /**     * 删除内存块,下次使用时将重新开辟内存块     * @return boolean     */    public function delete(){        if(($shm_id = shmop_open($this->shm_key,'c',$this->mode,$this->shm_size)) !== false){            $result = shmop_delete($shm_id);            shmop_close($shm_id);            return $result;        }        return false;    }}

复制代码

 

3.内置的web服务器类

  这个主要是嵌入在ftp的http服务器类,功能不是很完善,进行ftp的管理还是可行的。不过需要注意的是,这个实现与apache等其他http服务器运行的方式可能有所不同。代码是驻留内存的。

  

用PHP实现一个高效安全的ftp服务器

复制代码

class CWebServer{        protected $buffer_header = array();    protected $buffer_maxlen = 65535; //最大POST尺寸    const DATE_FORMAT_HTTP = 'D, d-M-Y H:i:s T';    const HTTP_EOF = "\r\n\r\n";    const HTTP_HEAD_MAXLEN = 8192; //http头最大长度不得超过2k    const HTTP_POST_MAXLEN = 1048576;//1m    const ST_FINISH = 1; //完成,进入处理流程    const ST_WAIT   = 2; //等待数据    const ST_ERROR  = 3; //错误,丢弃此包        private $requsts = array();        private $config = array();        public function log($msg,$level = 'debug'){        echo date('Y-m-d H:i:s').' ['.$level."]\t" .$msg."\n";    }        public function __construct($config = array()){        $this->config = array(                'wwwroot' => __DIR__.'/wwwroot/',                'index' => 'index.php',                'path_deny' => array('/protected/'),                        );              }        public function onReceive($serv,$fd,$data){                $ret = $this->checkData($fd, $data);        switch ($ret){            case self::ST_ERROR:                $serv->close($fd);                $this->cleanBuffer($fd);                $this->log('Recevie error.');                break;            case self::ST_WAIT:                 $this->log('Recevie wait.');                return;            default:                break;        }        //开始完整的请求        $request = $this->requsts[$fd];        $info = $serv->connection_info($fd);                 $request = $this->parseRequest($request);        $request['remote_ip'] = $info['remote_ip'];        $response = $this->onRequest($request);        $output = $this->parseResponse($request,$response);        $serv->send($fd,$output);        if(isset($request['head']['Connection']) && strtolower($request['head']['Connection']) == 'close'){            $serv->close($fd);        }        unset($this->requsts[$fd]);        $_REQUEST = $_SESSION = $_COOKIE = $_FILES = $_POST = $_SERVER = $_GET = array();    }        /**     * 处理请求     * @param array $request     * @return array $response     *      * $request=array(     *     'time'=>     *     'head'=>array(     *             'method'=>     *             'path'=>     *             'protocol'=>     *             'uri'=>     *             //other http header     *             '..'=>value     *         )     *  'body'=>     *  'get'=>(if appropriate)     *  'post'=>(if appropriate)     *  'cookie'=>(if appropriate)     *      *      * )     */    public function onRequest($request){                if($request['head']['path'][strlen($request['head']['path']) - 1] == '/'){            $request['head']['path'] .= $this->config['index'];        }        $response = $this->process($request);        return $response;    }             /**     * 清除数据     * @param unknown $fd     */    public function cleanBuffer($fd){        unset($this->requsts[$fd]);        unset($this->buffer_header[$fd]);    }        /**     * 检查数据     * @param unknown $fd     * @param unknown $data     * @return string     */    public function checkData($fd,$data){        if(isset($this->buffer_header[$fd])){            $data = $this->buffer_header[$fd].$data;        }        $request = $this->checkHeader($fd, $data);        //请求头错误        if($request === false){            $this->buffer_header[$fd] = $data;            if(strlen($data) > self::HTTP_HEAD_MAXLEN){                return self::ST_ERROR;            }else{                return self::ST_WAIT;            }        }        //post请求检查        if($request['head']['method'] == 'POST'){            return $this->checkPost($request);        }else{            return self::ST_FINISH;        }        }        /**     * 检查请求头     * @param unknown $fd     * @param unknown $data     * @return boolean|array     */    public function checkHeader($fd, $data){        //新的请求        if(!isset($this->requsts[$fd])){            //http头结束符            $ret = strpos($data,self::HTTP_EOF);            if($ret === false){                return false;            }else{                $this->buffer_header[$fd] = '';                $request = array();                list($header,$request['body']) = explode(self::HTTP_EOF, $data,2);                                $request['head'] = $this->parseHeader($header);                                            $this->requsts[$fd] = $request;                if($request['head'] == false){                    return false;                }            }        }else{            //post 数据合并            $request = $this->requsts[$fd];            $request['body'] .= $data;        }        return $request;    }        /**     * 解析请求头     * @param string $header     * @return array     * array(     *     'method'=>,     *     'uri'=>     *     'protocol'=>     *     'name'=>value,...     *          *          *      * }     */    public function parseHeader($header){        $request = array();        $headlines = explode("\r\n", $header);        list($request['method'],$request['uri'],$request['protocol']) = explode(' ', $headlines[0],3);                        foreach ($headlines as $k=>$line){            $line = trim($line);                        if($k && !empty($line) && strpos($line,':') !== false){                list($name,$value) = explode(':', $line,2);                $request[trim($name)] = trim($value);            }        }                return $request;    }        /**     * 检查post数据是否完整     * @param unknown $request     * @return string     */    public function checkPost($request){        if(isset($request['head']['Content-Length'])){            if(intval($request['head']['Content-Length']) > self::HTTP_POST_MAXLEN){                return self::ST_ERROR;            }            if(intval($request['head']['Content-Length']) > strlen($request['body'])){                return self::ST_WAIT;            }else{                return self::ST_FINISH;            }        }        return self::ST_ERROR;    }        /**     * 解析请求     * @param unknown $request     * @return Ambigous <unknown, mixed, multitype:string >     */    public function parseRequest($request){        $request['time'] = time();        $url_info = parse_url($request['head']['uri']);        $request['head']['path'] = $url_info['path'];        if(isset($url_info['fragment']))$request['head']['fragment'] = $url_info['fragment'];        if(isset($url_info['query'])){            parse_str($url_info['query'],$request['get']);        }        //parse post body        if($request['head']['method'] == 'POST'){            //目前只处理表单提交                        if (isset($request['head']['Content-Type']) && substr($request['head']['Content-Type'], 0, 33) == 'application/x-www-form-urlencoded'                || isset($request['head']['X-Request-With']) && $request['head']['X-Request-With'] == 'XMLHttpRequest'){                parse_str($request['body'],$request['post']);            }        }        //parse cookies        if(!empty($request['head']['Cookie'])){            $params = array();            $blocks = explode(";", $request['head']['Cookie']);            foreach ($blocks as $b){                $_r = explode("=", $b, 2);                if(count($_r)==2){                    list ($key, $value) = $_r;                    $params[trim($key)] = trim($value, "\r\n \t\"");                }else{                    $params[$_r[0]] = '';                }            }            $request['cookie'] = $params;        }        return $request;    }            public function parseResponse($request,$response){                        if(!isset($response['head']['Date'])){            $response['head']['Date'] = gmdate("D, d M Y H:i:s T");        }        if(!isset($response['head']['Content-Type'])){            $response['head']['Content-Type'] = 'text/html;charset=utf-8';        }        if(!isset($response['head']['Content-Length'])){            $response['head']['Content-Length'] = strlen($response['body']);        }        if(!isset($response['head']['Connection'])){            if(isset($request['head']['Connection']) && strtolower($request['head']['Connection']) == 'keep-alive'){                $response['head']['Connection'] = 'keep-alive';            }else{                $response['head']['Connection'] = 'close';            }                    }                $response['head']['Server'] = CFtpServer::$software.'/'.CFtpServer::VERSION;                        $out = '';        if(isset($response['head']['Status'])){            $out .= 'HTTP/1.1 '.$response['head']['Status']."\r\n";            unset($response['head']['Status']);        }else{            $out .= "HTTP/1.1 200 OK\r\n";        }        //headers        foreach($response['head'] as $k=>$v){            $out .= $k.': '.$v."\r\n";        }        //cookies        if($_COOKIE){                        $arr = array();            foreach ($_COOKIE as $k => $v){                $arr[] = $k.'='.$v;                        }            $out .= 'Set-Cookie: '.implode(';', $arr)."\r\n";        }        //End        $out .= "\r\n";        $out .= $response['body'];        return $out;    }    /**     * 处理请求     * @param unknown $request     * @return array     */    public function process($request){        $path = $request['head']['path'];        $isDeny = false;        foreach ($this->config['path_deny'] as $p){            if(strpos($path, $p) === 0){                $isDeny = true;                break;            }        }        if($isDeny){            return $this->httpError(403, '服务器拒绝访问:路径错误');                    }        if(!in_array($request['head']['method'],array('GET','POST'))){            return $this->httpError(500, '服务器拒绝访问:错误的请求方法');        }        $file_ext = strtolower(trim(substr(strrchr($path, '.'), 1)));        $path = realpath(rtrim($this->config['wwwroot'],'/'). '/' . ltrim($path,'/'));        $this->log('WEB:['.$request['head']['method'].'] '.$request['head']['uri'] .' '.json_encode(isset($request['post'])?$request['post']:array()));        $response = array();        if($file_ext == 'php'){            if(is_file($path)){                //设置全局变量                                if(isset($request['get']))$_GET = $request['get'];                if(isset($request['post']))$_POST = $request['post'];                if(isset($request['cookie']))$_COOKIE = $request['cookie'];                $_REQUEST = array_merge($_GET,$_POST, $_COOKIE);                                foreach ($request['head'] as $key => $value){                    $_key = 'HTTP_'.strtoupper(str_replace('-', '_', $key));                    $_SERVER[$_key] = $value;                }                $_SERVER['REMOTE_ADDR'] = $request['remote_ip'];                $_SERVER['REQUEST_URI'] = $request['head']['uri'];                    //进行http auth                if(isset($_GET['c']) && strtolower($_GET['c']) != 'site'){                    if(isset($request['head']['Authorization'])){                        $user = new User();                        if($user->checkUserBasicAuth($request['head']['Authorization'])){                            $response['head']['Status'] = self::$HTTP_HEADERS[200];                            goto process;                        }                    }                    $response['head']['Status'] = self::$HTTP_HEADERS[401];                    $response['head']['WWW-Authenticate'] = 'Basic realm="Real-Data-FTP"';                        $_GET['c'] = 'Site';                    $_GET['a'] = 'Unauthorized';                                     }                process:                            ob_start();                                        try{                    include  $path;                                        $response['body'] = ob_get_contents();                    $response['head']['Content-Type'] = APP::$content_type;                                    }catch (Exception $e){                    $response = $this->httpError(500, $e->getMessage());                }                ob_end_clean();            }else{                $response = $this->httpError(404, '页面不存在');            }        }else{            //处理静态文件            if(is_file($path)){                $response['head']['Content-Type'] = isset(self::$MIME_TYPES[$file_ext]) ? self::$MIME_TYPES[$file_ext]:"application/octet-stream";                //使用缓存                if(!isset($request['head']['If-Modified-Since'])){                    $fstat = stat($path);                    $expire = 2592000;//30 days                    $response['head']['Status'] = self::$HTTP_HEADERS[200];                    $response['head']['Cache-Control'] = "max-age={$expire}";                    $response['head']['Pragma'] = "max-age={$expire}";                    $response['head']['Last-Modified'] = date(self::DATE_FORMAT_HTTP, $fstat['mtime']);                    $response['head']['Expires'] = "max-age={$expire}";                    $response['body'] = file_get_contents($path);                }else{                    $response['head']['Status'] = self::$HTTP_HEADERS[304];                    $response['body'] = '';                }                            }else{                $response = $this->httpError(404, '页面不存在');            }                }        return $response;    }        public function httpError($code, $content){        $response = array();        $version = CFtpServer::$software.'/'.CFtpServer::VERSION;                $response['head']['Content-Type'] = 'text/html;charset=utf-8';        $response['head']['Status'] = self::$HTTP_HEADERS[$code];        $response['body'] = <<<html<!DOCTYPE html><html lang="zh-CN">  <head>    <meta charset="utf-8">      <title>FTP后台管理 </title>  </head>  <body>    <p>{$content}</p>    <div style="text-align:center">    <hr>        {$version} Copyright &copy; 2015 by <a target='_new' href='http://www.realdatamed.com'>Real Data</a> All Rights Reserved.    </div>  </body></html>html;        return $response;    }            static $HTTP_HEADERS = array(            100 => "100 Continue",            101 => "101 Switching Protocols",            200 => "200 OK",            201 => "201 Created",            204 => "204 No Content",            206 => "206 Partial Content",            300 => "300 Multiple Choices",            301 => "301 Moved Permanently",            302 => "302 Found",            303 => "303 See Other",            304 => "304 Not Modified",            307 => "307 Temporary Redirect",            400 => "400 Bad Request",            401 => "401 Unauthorized",            403 => "403 Forbidden",            404 => "404 Not Found",            405 => "405 Method Not Allowed",            406 => "406 Not Acceptable",            408 => "408 Request Timeout",            410 => "410 Gone",            413 => "413 Request Entity Too Large",            414 => "414 Request URI Too Long",            415 => "415 Unsupported Media Type",            416 => "416 Requested Range Not Satisfiable",            417 => "417 Expectation Failed",            500 => "500 Internal Server Error",            501 => "501 Method Not Implemented",            503 => "503 Service Unavailable",            506 => "506 Variant Also Negotiates",    );        static $MIME_TYPES = array(                    'jpg' => 'image/jpeg',        'bmp' => 'image/bmp',        'ico' => 'image/x-icon',        'gif' => 'image/gif',        'png' => 'image/png' ,        'bin' => 'application/octet-stream',        'js' => 'application/javascript',        'css' => 'text/css' ,        'html' => 'text/html' ,        'xml' => 'text/xml',        'tar' => 'application/x-tar' ,        'ppt' => 'application/vnd.ms-powerpoint',        'pdf' => 'application/pdf' ,        'svg' => ' image/svg+xml',        'woff' => 'application/x-font-woff',        'woff2' => 'application/x-font-woff',                    );    }

复制代码

4.FTP主类

  有了前面类,就可以在ftp进行引用了。使用ssl时,请注意进行防火墙passive 端口范围的nat配置。

  

用PHP实现一个高效安全的ftp服务器

复制代码

defined('DEBUG_ON') or define('DEBUG_ON', false);//主目录defined('BASE_PATH') or define('BASE_PATH', __DIR__);require_once BASE_PATH.'/inc/User.php';require_once BASE_PATH.'/inc/ShareMemory.php';require_once BASE_PATH.'/web/CWebServer.php';require_once BASE_PATH.'/inc/CSmtp.php';class CFtpServer{    //软件版本    const VERSION = '2.0';            const EOF = "\r\n";            public static $software "FTP-Server";        private static $server_mode = SWOOLE_PROCESS;            private static $pid_file;        private static $log_file;            //待写入文件的日志队列(缓冲区)    private $queue = array();        private $pasv_port_range = array(55000,60000);        public $host = '0.0.0.0';        public $port = 21;                public $setting = array();        //最大连接数    public $max_connection = 50;            //web管理端口    public $manager_port = 8080;        //tls    public $ftps_port = 990;        /**     * @var swoole_server     */    protected $server;        protected $connection = array();        protected $session = array();        protected $user;//用户类,复制验证与权限        //共享内存类    protected $shm;//ShareMemory        /**     *      * @var embedded http server     */    protected $webserver;                /*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++      + 静态方法      +++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/        public static function setPidFile($pid_file){        self::$pid_file = $pid_file;    }        /**     * 服务启动控制方法     */    public static function start($startFunc){        if(empty(self::$pid_file)){            exit("Require pid file.\n");                    }        if(!extension_loaded('posix')){                        exit("Require extension `posix`.\n");                    }        if(!extension_loaded('swoole')){                        exit("Require extension `swoole`.\n");                    }        if(!extension_loaded('shmop')){            exit("Require extension `shmop`.\n");        }        if(!extension_loaded('openssl')){            exit("Require extension `openssl`.\n");        }                        $pid_file = self::$pid_file;        $server_pid = 0;        if(is_file($pid_file)){            $server_pid = file_get_contents($pid_file);        }        global $argv;        if(empty($argv[1])){            goto usage;        }elseif($argv[1] == 'reload'){            if (empty($server_pid)){                exit("FtpServer is not running\n");            }            posix_kill($server_pid, SIGUSR1);            exit;        }elseif ($argv[1] == 'stop'){            if (empty($server_pid)){                exit("FtpServer is not running\n");            }            posix_kill($server_pid, SIGTERM);            exit;        }elseif ($argv[1] == 'start'){            //已存在ServerPID,并且进程存在            if (!empty($server_pid) and posix_kill($server_pid,(int) 0)){                exit("FtpServer is already running.\n");            }            //启动服务器            $startFunc();                    }else{            usage:            exit("Usage: php {$argv[0]} start|stop|reload\n");        }            }        /*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++      + 方法      +++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/        public function __construct($host,$port){        $this->user = new User();        $this->shm = new ShareMemory();        $this->shm->write(array());        $flag = SWOOLE_SOCK_TCP;        $this->server = new swoole_server($host,$port,self::$server_mode,$flag);        $this->host = $host;        $this->port = $port;        $this->setting = array(                'backlog' => 128,                                'dispatch_mode' => 2,            );            }        public function daemonize(){        $this->setting['daemonize'] = 1;        }        public function getConnectionInfo($fd){        return $this->server->connection_info($fd);            }        /**     * 启动服务进程     * @param array $setting     * @throws Exception     */        public function run($setting = array()){        $this->setting = array_merge($this->setting,$setting);                //不使用swoole的默认日志        if(isset($this->setting['log_file'])){            self::$log_file = $this->setting['log_file'];            unset($this->setting['log_file']);        }            if(isset($this->setting['max_connection'])){            $this->max_connection = $this->setting['max_connection'];            unset($this->setting['max_connection']);        }        if(isset($this->setting['manager_port'])){            $this->manager_port = $this->setting['manager_port'];            unset($this->setting['manager_port']);        }        if(isset($this->setting['ftps_port'])){            $this->ftps_port = $this->setting['ftps_port'];            unset($this->setting['ftps_port']);        }        if(isset($this->setting['passive_port_range'])){            $this->pasv_port_range = $this->setting['passive_port_range'];            unset($this->setting['passive_port_range']);        }                        $this->server->set($this->setting);        $version = explode('.', SWOOLE_VERSION);        if($version[0] == 1 && $version[1] < 7 && $version[2] <20){            throw new Exception('Swoole version require 1.7.20 +.');        }        //事件绑定        $this->server->on('start',array($this,'onMasterStart'));        $this->server->on('shutdown',array($this,'onMasterStop'));        $this->server->on('ManagerStart',array($this,'onManagerStart'));        $this->server->on('ManagerStop',array($this,'onManagerStop'));        $this->server->on('WorkerStart',array($this,'onWorkerStart'));        $this->server->on('WorkerStop',array($this,'onWorkerStop'));        $this->server->on('WorkerError',array($this,'onWorkerError'));        $this->server->on('Connect',array($this,'onConnect'));        $this->server->on('Receive',array($this,'onReceive'));        $this->server->on('Close',array($this,'onClose'));        //管理端口        $this->server->addlistener($this->host,$this->manager_port,SWOOLE_SOCK_TCP);        //tls        $this->server->addlistener($this->host,$this->ftps_port,SWOOLE_SOCK_TCP | SWOOLE_SSL);                $this->server->start();    }        public function log($msg,$level = 'debug',$flush = false){                if(DEBUG_ON){            $log = date('Y-m-d H:i:s').' ['.$level."]\t" .$msg."\n";            if(!empty(self::$log_file)){                $debug_file = dirname(self::$log_file).'/debug.log';                                file_put_contents($debug_file, $log,FILE_APPEND);                if(filesize($debug_file) > 10485760){//10M                    unlink($debug_file);                }            }            echo $log;                    }        if($level != 'debug'){            //日志记录                                    $this->queue[] = date('Y-m-d H:i:s')."\t[".$level."]\t".$msg;            }            if(count($this->queue)>10 && !empty(self::$log_file) || $flush){            if (filesize(self::$log_file) > 209715200){ //200M                            rename(self::$log_file,self::$log_file.'.'.date('His'));            }            $logs = '';            foreach ($this->queue as $q){                $logs .= $q."\n";            }            file_put_contents(self::$log_file, $logs,FILE_APPEND);            $this->queue = array();        }            }        public function shutdown(){        return $this->server->shutdown();    }        public function close($fd){        return $this->server->close($fd);    }        public function send($fd,$data){        $data = strtr($data,array("\n" => "", "\0" => "", "\r" => ""));        $this->log("[-->]\t" . $data);        return $this->server->send($fd,$data.self::EOF);    }                /*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++      + 事件回调      +++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/        public function onMasterStart($serv){        global $argv;        swoole_set_process_name('php '.$argv[0].': master -host='.$this->host.' -port='.$this->port.'/'.$this->manager_port);        if(!empty($this->setting['pid_file'])){            file_put_contents(self::$pid_file, $serv->master_pid);        }        $this->log('Master started.');    }        public function onMasterStop($serv){        if (!empty($this->setting['pid_file'])){            unlink(self::$pid_file);        }        $this->shm->delete();        $this->log('Master stop.');    }        public function onManagerStart($serv){        global $argv;        swoole_set_process_name('php '.$argv[0].': manager');        $this->log('Manager started.');    }        public function onManagerStop($serv){        $this->log('Manager stop.');    }        public function onWorkerStart($serv,$worker_id){        global $argv;        if($worker_id >= $serv->setting['worker_num']) {            swoole_set_process_name("php {$argv[0]}: worker [task]");        } else {            swoole_set_process_name("php {$argv[0]}: worker [{$worker_id}]");        }        $this->log("Worker {$worker_id} started.");    }        public function onWorkerStop($serv,$worker_id){        $this->log("Worker {$worker_id} stop.");    }        public function onWorkerError($serv,$worker_id,$worker_pid,$exit_code){        $this->log("Worker {$worker_id} error:{$exit_code}.");    }        public function onConnect($serv,$fd,$from_id){        $info = $this->getConnectionInfo($fd);        if($info['server_port'] == $this->manager_port){            //web请求            $this->webserver = new CWebServer();        }else{            $this->send($fd, "220---------- Welcome to " . self::$software . " ----------");            $this->send($fd, "220-Local time is now " . date("H:i"));            $this->send($fd, "220 This is a private system - No anonymous login");            if(count($this->server->connections) <= $this->max_connection){                if($info['server_port'] == $this->port && isset($this->setting['force_ssl']) && $this->setting['force_ssl']){                    //如果启用强制ssl                                        $this->send($fd, "421 Require implicit FTP over tls, closing control connection.");                    $this->close($fd);                    return ;                }                $this->connection[$fd] = array();                $this->session = array();                $this->queue = array();                            }else{                                        $this->send($fd, "421 Too many connections, closing control connection.");                $this->close($fd);            }        }    }        public function onReceive($serv,$fd,$from_id,$recv_data){        $info = $this->getConnectionInfo($fd);                if($info['server_port'] == $this->manager_port){            //web请求            $this->webserver->onReceive($this->server, $fd, $recv_data);        }else{            $read = trim($recv_data);            $this->log("[<--]\t" . $read);            $cmd = explode(" ", $read);                    $func = 'cmd_'.strtoupper($cmd[0]);            $data = trim(str_replace($cmd[0], '', $read));            if (!method_exists($this, $func)){                $this->send($fd, "500 Unknown Command");                return;            }            if (empty($this->connection[$fd]['login'])){                switch($cmd[0]){                    case 'TYPE':                    case 'USER':                    case 'PASS':                    case 'QUIT':                    case 'AUTH':                    case 'PBSZ':                        break;                    default:                        $this->send($fd,"530 You aren't logged in");                        return;                }            }            $this->$func($fd,$data);        }    }            public function onClose($serv,$fd,$from_id){        //在线用户                $shm_data = $this->shm->read();        if($shm_data !== false){            if(isset($shm_data['online'])){                $list = array();                foreach($shm_data['online'] as $u => $info){                                        if(!preg_match('/\.*-'.$fd.'$/',$u,$m))                        $list[$u] = $info;                }                $shm_data['online'] = $list;                $this->shm->write($shm_data);                            }                    }                $this->log('Socket '.$fd.' close. Flush the logs.','debug',true);    }        /*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++      + 工具函数      +++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/            /**     * 获取用户名     * @param  $fd     */    public function getUser($fd){        return isset($this->connection[$fd]['user'])?$this->connection[$fd]['user']:'';    }            /**     * 获取文件全路径     * @param  $user     * @param  $file     * @return string|boolean     */    public function getFile($user, $file){        $file = $this->fillDirName($user, $file);                if (is_file($file)){            return realpath($file);        }else{            return false;        }    }            /**     * 遍历目录     * @param $rdir     * @param $showHidden     * @param $format list/mlsd     * @return string     *      * list 使用local时间     * mlsd 使用gmt时间     */    public function getFileList($user, $rdir, $showHidden = false, $format = 'list'){        $filelist = '';        if($format == 'mlsd'){            $stats = stat($rdir);            $filelist.= 'Type=cdir;Modify='.gmdate('YmdHis',$stats['mtime']).';UNIX.mode=d'.$this->mode2char($stats['mode']).'; '.$this->getUserDir($user)."\r\n";        }        if ($handle = opendir($rdir)){            $isListable = $this->user->isFolderListable($user, $rdir);            while (false !== ($file = readdir($handle))){                if ($file == '.' or $file == '..'){                    continue;                }                if ($file{0} == "." and !$showHidden){                    continue;                }                //如果当前目录$rdir不允许列出,则判断当前目录下的目录是否配置为可以列出                                if(!$isListable){                        $dir = $rdir . $file;                    if(is_dir($dir)){                        $dir = $this->joinPath($dir, '/');                        if($this->user->isFolderListable($user, $dir)){                                                        goto listFolder;                        }                    }                    continue;                }                    listFolder:                            $stats = stat($rdir . $file);                if (is_dir($rdir . "/" . $file)) $mode = "d"; else $mode = "-";                $mode .= $this->mode2char($stats['mode']);                if($format == 'mlsd'){                    if($mode[0] == 'd'){                        $filelist.= 'Type=dir;Modify='.gmdate('YmdHis',$stats['mtime']).';UNIX.mode='.$mode.'; '.$file."\r\n";                    }else{                        $filelist.= 'Type=file;Size='.$stats['size'].';Modify='.gmdate('YmdHis',$stats['mtime']).';UNIX.mode='.$mode.'; '.$file."\r\n";                    }                }else{                    $uidfill = "";                    for ($i = strlen($stats['uid']); $i < 5; $i++) $uidfill .= " ";                    $gidfill = "";                    for ($i = strlen($stats['gid']); $i < 5; $i++) $gidfill .= " ";                    $sizefill = "";                    for ($i = strlen($stats['size']); $i < 11; $i++) $sizefill .= " ";                    $nlinkfill = "";                    for ($i = strlen($stats['nlink']); $i < 5; $i++) $nlinkfill .= " ";                    $mtime = date("M d H:i", $stats['mtime']);                    $filelist .= $mode . $nlinkfill . $stats['nlink'] . " " . $stats['uid'] . $uidfill . $stats['gid'] . $gidfill . $sizefill . $stats['size'] . " " . $mtime . " " . $file . "\r\n";                }            }            closedir($handle);        }        return $filelist;    }        /**     * 将文件的全新从数字转换为字符串     * @param int $int     */    public function mode2char($int){        $mode = '';        $moded = sprintf("%o", ($int & 000777));        $mode1 = substr($moded, 0, 1);        $mode2 = substr($moded, 1, 1);        $mode3 = substr($moded, 2, 1);        switch ($mode1) {            case "0":                $mode .= "---";                break;            case "1":                $mode .= "--x";                break;            case "2":                $mode .= "-w-";                break;            case "3":                $mode .= "-wx";                break;            case "4":                $mode .= "r--";                break;            case "5":                $mode .= "r-x";                break;            case "6":                $mode .= "rw-";                break;            case "7":                $mode .= "rwx";                break;        }        switch ($mode2) {            case "0":                $mode .= "---";                break;            case "1":                $mode .= "--x";                break;            case "2":                $mode .= "-w-";                break;            case "3":                $mode .= "-wx";                break;            case "4":                $mode .= "r--";                break;            case "5":                $mode .= "r-x";                break;            case "6":                $mode .= "rw-";                break;            case "7":                $mode .= "rwx";                break;        }        switch ($mode3) {            case "0":                $mode .= "---";                break;            case "1":                $mode .= "--x";                break;            case "2":                $mode .= "-w-";                break;            case "3":                $mode .= "-wx";                break;            case "4":                $mode .= "r--";                break;            case "5":                $mode .= "r-x";                break;            case "6":                $mode .= "rw-";                break;            case "7":                $mode .= "rwx";                break;        }        return $mode;    }        /**     * 设置用户当前的路径         * @param $user     * @param $pwd     */    public function setUserDir($user, $cdir){        $old_dir = $this->session[$user]['pwd'];        if ($old_dir == $cdir){            return $cdir;        }            if($cdir[0] != '/')            $cdir = $this->joinPath($old_dir,$cdir);                $this->session[$user]['pwd'] = $cdir;        $abs_dir = realpath($this->getAbsDir($user));        if (!$abs_dir){            $this->session[$user]['pwd'] = $old_dir;            return false;        }        $this->session[$user]['pwd'] = $this->joinPath('/',substr($abs_dir, strlen($this->session[$user]['home'])));        $this->session[$user]['pwd'] = $this->joinPath($this->session[$user]['pwd'],'/');        $this->log("CHDIR: $old_dir -> $cdir");        return $this->session[$user]['pwd'];    }        /**     * 获取全路径     * @param $user     * @param $file     * @return string     */    public function fillDirName($user, $file){                if (substr($file, 0, 1) != "/"){            $file = '/'.$file;            $file = $this->joinPath($this->getUserDir( $user), $file);        }                        $file = $this->joinPath($this->session[$user]['home'],$file);        return $file;    }        /**     * 获取用户路径     * @param unknown $user     */    public function getUserDir($user){        return $this->session[$user]['pwd'];    }        /**     * 获取用户的当前文件系统绝对路径,非chroot路径     * @param $user     * @return string     */    public function getAbsDir($user){        $rdir = $this->joinPath($this->session[$user]['home'],$this->session[$user]['pwd']);        return $rdir;    }        /**     * 路径连接     * @param string $path1     * @param string $path2     * @return string     */    public function joinPath($path1,$path2){                $path1 = rtrim($path1,'/');        $path2 = trim($path2,'/');        return $path1.'/'.$path2;    }        /**     * IP判断     * @param string $ip     * @return boolean     */    public function isIPAddress($ip){        if (!is_numeric($ip[0]) || $ip[0] < 1 || $ip[0] > 254) {            return false;        } elseif (!is_numeric($ip[1]) || $ip[1] < 0 || $ip[1] > 254) {            return false;        } elseif (!is_numeric($ip[2]) || $ip[2] < 0 || $ip[2] > 254) {            return false;        } elseif (!is_numeric($ip[3]) || $ip[3] < 1 || $ip[3] > 254) {            return false;        } elseif (!is_numeric($ip[4]) || $ip[4] < 1 || $ip[4] > 500) {            return false;        } elseif (!is_numeric($ip[5]) || $ip[5] < 1 || $ip[5] > 500) {            return false;        } else {            return true;        }    }        /**     * 获取pasv端口     * @return number     */    public function getPasvPort(){        $min = is_int($this->pasv_port_range[0])?$this->pasv_port_range[0]:55000;        $max = is_int($this->pasv_port_range[1])?$this->pasv_port_range[1]:60000;        $max = $max <= 65535 ? $max : 65535;        $loop = 0;        $port = 0;        while($loop < 10){            $port = mt_rand($min, $max);            if($this->isAvailablePasvPort($port)){                                break;            }            $loop++;        }                return $port;    }        public function pushPasvPort($port){        $shm_data = $this->shm->read();        if($shm_data !== false){            if(isset($shm_data['pasv_port'])){                array_push($shm_data['pasv_port'], $port);            }else{                $shm_data['pasv_port'] = array($port);            }            $this->shm->write($shm_data);            $this->log('Push pasv port: '.implode(',', $shm_data['pasv_port']));            return true;        }        return false;    }        public function popPasvPort($port){        $shm_data = $this->shm->read();        if($shm_data !== false){            if(isset($shm_data['pasv_port'])){                $tmp = array();                foreach ($shm_data['pasv_port'] as $p){                    if($p != $port){                        $tmp[] = $p;                    }                }                $shm_data['pasv_port'] = $tmp;            }            $this->shm->write($shm_data);            $this->log('Pop pasv port: '.implode(',', $shm_data['pasv_port']));            return true;        }        return false;    }        public function isAvailablePasvPort($port){        $shm_data = $this->shm->read();        if($shm_data !== false){            if(isset($shm_data['pasv_port'])){                return !in_array($port, $shm_data['pasv_port']);            }            return true;        }        return false;    }        /**     * 获取当前数据链接tcp个数     */    public function getDataConnections(){        $shm_data = $this->shm->read();        if($shm_data !== false){            if(isset($shm_data['pasv_port'])){                return count($shm_data['pasv_port']);            }                    }        return 0;    }            /**     * 关闭数据传输socket     * @param $user     * @return bool     */    public function closeUserSock($user){        $peer = stream_socket_get_name($this->session[$user]['sock'], false);        list($ip,$port) = explode(':', $peer);        //释放端口占用        $this->popPasvPort($port);        fclose($this->session[$user]['sock']);        $this->session[$user]['sock'] = 0;        return true;    }        /**     * @param $user     * @return resource     */    public function getUserSock($user){        //被动模式        if ($this->session[$user]['pasv'] == true){            if (empty($this->session[$user]['sock'])){                $addr = stream_socket_get_name($this->session[$user]['serv_sock'], false);                list($ip, $port) = explode(':', $addr);                $sock = stream_socket_accept($this->session[$user]['serv_sock'], 5);                if ($sock){                    $peer = stream_socket_get_name($sock, true);                    $this->log("Accept: success client is $peer.");                    $this->session[$user]['sock'] = $sock;                    //关闭server socket                    fclose($this->session[$user]['serv_sock']);                }else{                    $this->log("Accept: failed.");                    //释放端口                    $this->popPasvPort($port);                    return false;                }            }        }        return $this->session[$user]['sock'];    }                /*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++      + FTP Command      +++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/        //==================    //RFC959    //==================    /**     * 登录用户名     * @param $fd     * @param $data     */    public function cmd_USER($fd, $data){        if (preg_match("/^([a-z0-9.@]+)$/", $data)){            $user = strtolower($data);            $this->connection[$fd]['user'] = $user;                        $this->send($fd, "331 User $user OK. Password required");        }else{            $this->send($fd, "530 Login authentication failed");        }    }        /**     * 登录密码     * @param $fd     * @param $data     */    public function cmd_PASS($fd, $data){        $user = $this->connection[$fd]['user'];        $pass = $data;        $info = $this->getConnectionInfo($fd);        $ip = $info['remote_ip'];        //判断登陆失败次数        if($this->user->isAttemptLimit($this->shm, $user, $ip)){            $this->send($fd, "530 Login authentication failed: Too many login attempts. Blocked in 10 minutes.");            return;        }            if ($this->user->checkUser($user, $pass, $ip)){            $dir = "/";            $this->session[$user]['pwd'] = $dir;            //ftp根目录                        $this->session[$user]['home'] = $this->user->getHomeDir($user);            if(empty($this->session[$user]['home']) || !is_dir($this->session[$user]['home'])){                $this->send($fd, "530 Login authentication failed: `home` path error.");            }else{                $this->connection[$fd]['login'] = true;                //在线用户                $shm_data = $this->user->addOnline($this->shm, $this->server, $user, $fd, $ip);                $this->log('SHM: '.json_encode($shm_data) );                                $this->send($fd, "230 OK. Current restricted directory is " . $dir);                                $this->log('User '.$user .' has login successfully! IP: '.$ip,'warn');            }        }else{            $this->user->addAttempt($this->shm, $user, $ip);            $this->log('User '.$user .' login fail! IP: '.$ip,'warn');            $this->send($fd, "530 Login authentication failed: check your pass or ip allow rules.");        }    }        /**     * 更改当前目录     * @param $fd     * @param $data     */    public function cmd_CWD($fd, $data){        $user = $this->getUser($fd);        if (($dir = $this->setUserDir($user, $data)) != false){            $this->send($fd, "250 OK. Current directory is " . $dir);        }else{            $this->send($fd, "550 Can't change directory to " . $data . ": No such file or directory");        }    }        /**     * 返回上级目录     * @param  $fd     * @param  $data     */    public function cmd_CDUP($fd, $data){        $data = '..';        $this->cmd_CWD($fd, $data);    }            /**     * 退出服务器     * @param $fd     * @param $data     */    public function cmd_QUIT($fd, $data){        $this->send($fd,"221 Goodbye.");        unset($this->connection[$fd]);    }            /**     * 获取当前目录     * @param $fd     * @param $data     */    public function cmd_PWD($fd, $data){        $user = $this->getUser($fd);        $this->send($fd, "257 \"" . $this->getUserDir($user) . "\" is your current location");    }        /**     * 下载文件     * @param $fd     * @param $data     */    public function cmd_RETR($fd, $data){        $user = $this->getUser($fd);        $ftpsock = $this->getUserSock($user);        if (!$ftpsock){            $this->send($fd, "425 Connection Error");            return;        }        if (($file = $this->getFile($user, $data)) != false){            if($this->user->isReadable($user, $file)){                $this->send($fd, "150 Connecting to client");                if ($fp = fopen($file, "rb")){                    //断点续传                    if(isset($this->session[$user]['rest_offset'])){                        if(!fseek($fp, $this->session[$user]['rest_offset'])){                            $this->log("RETR at offset ".ftell($fp));                        }else{                            $this->log("RETR at offset ".ftell($fp).' fail.');                        }                        unset($this->session[$user]['rest_offset']);                    }                                        while (!feof($fp)){                                                $cont = fread($fp, 8192);                                                if (!fwrite($ftpsock, $cont)) break;                                            }                    if (fclose($fp) and $this->closeUserSock($user)){                        $this->send($fd, "226 File successfully transferred");                        $this->log($user."\tGET:".$file,'info');                    }else{                        $this->send($fd, "550 Error during file-transfer");                    }                }else{                    $this->send($fd, "550 Can't open " . $data . ": Permission denied");                }            }else{                $this->send($fd, "550 You're unauthorized: Permission denied");            }        }else{            $this->send($fd, "550 Can't open " . $data . ": No such file or directory");        }    }        /**     * 上传文件     * @param $fd     * @param $data     */    public function cmd_STOR($fd, $data){        $user = $this->getUser($fd);        $ftpsock = $this->getUserSock($user);        if (!$ftpsock){            $this->send($fd, "425 Connection Error");            return;        }        $file = $this->fillDirName($user, $data);        $isExist = false;        if(file_exists($file))$isExist = true;        if((!$isExist && $this->user->isWritable($user, $file)) ||            ($isExist && $this->user->isAppendable($user, $file))){            if($isExist){                $fp = fopen($file, "rb+");                $this->log("OPEN for STOR.");            }else{                $fp = fopen($file, 'wb');                $this->log("CREATE for STOR.");            }            if (!$fp){                $this->send($fd, "553 Can't open that file: Permission denied");            }else{                //断点续传,需要Append权限                if(isset($this->session[$user]['rest_offset'])){                    if(!fseek($fp, $this->session[$user]['rest_offset'])){                        $this->log("STOR at offset ".ftell($fp));                    }else{                        $this->log("STOR at offset ".ftell($fp).' fail.');                    }                    unset($this->session[$user]['rest_offset']);                }                $this->send($fd, "150 Connecting to client");                while (!feof($ftpsock)){                    $cont = fread($ftpsock, 8192);                    if (!$cont) break;                    if (!fwrite($fp, $cont)) break;                }                touch($file);//设定文件的访问和修改时间                if (fclose($fp) and $this->closeUserSock($user)){                    $this->send($fd, "226 File successfully transferred");                    $this->log($user."\tPUT: $file",'info');                }else{                    $this->send($fd, "550 Error during file-transfer");                }            }        }else{            $this->send($fd, "550 You're unauthorized: Permission denied");            $this->closeUserSock($user);        }    }        /**     * 文件追加     * @param $fd     * @param $data     */    public function cmd_APPE($fd,$data){        $user = $this->getUser($fd);        $ftpsock = $this->getUserSock($user);        if (!$ftpsock){            $this->send($fd, "425 Connection Error");            return;        }        $file = $this->fillDirName($user, $data);        $isExist = false;        if(file_exists($file))$isExist = true;        if((!$isExist && $this->user->isWritable($user, $file)) ||        ($isExist && $this->user->isAppendable($user, $file))){            $fp = fopen($file, "rb+");            if (!$fp){                $this->send($fd, "553 Can't open that file: Permission denied");            }else{                //断点续传,需要Append权限                if(isset($this->session[$user]['rest_offset'])){                    if(!fseek($fp, $this->session[$user]['rest_offset'])){                        $this->log("APPE at offset ".ftell($fp));                    }else{                        $this->log("APPE at offset ".ftell($fp).' fail.');                    }                    unset($this->session[$user]['rest_offset']);                }                $this->send($fd, "150 Connecting to client");                while (!feof($ftpsock)){                    $cont = fread($ftpsock, 8192);                    if (!$cont) break;                    if (!fwrite($fp, $cont)) break;                }                touch($file);//设定文件的访问和修改时间                if (fclose($fp) and $this->closeUserSock($user)){                    $this->send($fd, "226 File successfully transferred");                    $this->log($user."\tAPPE: $file",'info');                }else{                    $this->send($fd, "550 Error during file-transfer");                }            }        }else{            $this->send($fd, "550 You're unauthorized: Permission denied");            $this->closeUserSock($user);        }    }        /**     * 文件重命名,源文件     * @param $fd     * @param $data     */    public function cmd_RNFR($fd, $data){        $user = $this->getUser($fd);        $file = $this->fillDirName($user, $data);        if (file_exists($file) || is_dir($file)){            $this->session[$user]['rename'] = $file;            $this->send($fd, "350 RNFR accepted - file exists, ready for destination");                    }else{            $this->send($fd, "550 Sorry, but that '$data' doesn't exist");        }    }    /**     * 文件重命名,目标文件     * @param $fd     * @param $data     */    public function cmd_RNTO($fd, $data){        $user = $this->getUser($fd);        $old_file = $this->session[$user]['rename'];        $new_file = $this->fillDirName($user, $data);        $isDir = false;        if(is_dir($old_file)){            $isDir = true;            $old_file = $this->joinPath($old_file, '/');        }        if((!$isDir && $this->user->isRenamable($user, $old_file)) ||             ($isDir && $this->user->isFolderRenamable($user, $old_file))){            if (empty($old_file) or !is_dir(dirname($new_file))){                $this->send($fd, "451 Rename/move failure: No such file or directory");            }elseif (rename($old_file, $new_file)){                $this->send($fd, "250 File successfully renamed or moved");                $this->log($user."\tRENAME: $old_file to $new_file",'warn');            }else{                $this->send($fd, "451 Rename/move failure: Operation not permitted");            }        }else{            $this->send($fd, "550 You're unauthorized: Permission denied");        }        unset($this->session[$user]['rename']);    }        /**     * 删除文件     * @param $fd     * @param $data     */    public function cmd_DELE($fd, $data){        $user = $this->getUser($fd);        $file = $this->fillDirName($user, $data);        if($this->user->isDeletable($user, $file)){            if (!file_exists($file)){                $this->send($fd, "550 Could not delete " . $data . ": No such file or directory");            }            elseif (unlink($file)){                $this->send($fd, "250 Deleted " . $data);                $this->log($user."\tDEL: $file",'warn');            }else{                $this->send($fd, "550 Could not delete " . $data . ": Permission denied");            }        }else{            $this->send($fd, "550 You're unauthorized: Permission denied");        }    }        /**     * 创建目录     * @param $fd     * @param $data     */    public function cmd_MKD($fd, $data){        $user = $this->getUser($fd);        $path = '';        if($data[0] == '/'){            $path  = $this->joinPath($this->session[$user]['home'],$data);        }else{            $path = $this->joinPath($this->getAbsDir($user),$data);        }        $path = $this->joinPath($path, '/');                if($this->user->isFolderCreatable($user, $path)){            if (!is_dir(dirname($path))){                $this->send($fd, "550 Can't create directory: No such file or directory");            }elseif(file_exists($path)){                $this->send($fd, "550 Can't create directory: File exists");            }else{                if (mkdir($path)){                    $this->send($fd, "257 \"" . $data . "\" : The directory was successfully created");                    $this->log($user."\tMKDIR: $path",'info');                }else{                    $this->send($fd, "550 Can't create directory: Permission denied");                }            }        }else{            $this->send($fd, "550 You're unauthorized: Permission denied");        }    }        /**     * 删除目录     * @param $fd     * @param $data     */    public function cmd_RMD($fd, $data){        $user = $this->getUser($fd);        $dir = '';        if($data[0] == '/'){            $dir = $this->joinPath($this->session[$user]['home'], $data);        }else{            $dir = $this->fillDirName($user, $data);        }        $dir = $this->joinPath($dir, '/');        if($this->user->isFolderDeletable($user, $dir)){            if (is_dir(dirname($dir)) and is_dir($dir)){                if (count(glob($dir . "/*"))){                    $this->send($fd, "550 Can't remove directory: Directory not empty");                }elseif (rmdir($dir)){                    $this->send($fd, "250 The directory was successfully removed");                    $this->log($user."\tRMDIR: $dir",'warn');                }else{                    $this->send($fd, "550 Can't remove directory: Operation not permitted");                }            }elseif (is_dir(dirname($dir)) and file_exists($dir)){                $this->send($fd, "550 Can't remove directory: Not a directory");            }else{                $this->send($fd, "550 Can't create directory: No such file or directory");            }        }else{            $this->send($fd, "550 You're unauthorized: Permission denied");        }    }                /**     * 得到服务器类型     * @param $fd     * @param $data     */    public function cmd_SYST($fd, $data){        $this->send($fd, "215 UNIX Type: L8");    }                        /**     * 权限控制     * @param $fd     * @param $data     */    public function cmd_SITE($fd, $data){        if (substr($data, 0, 6) == "CHMOD "){            $user = $this->getUser($fd);            $chmod = explode(" ", $data, 3);            $file = $this->fillDirName($user, $chmod[2]);            if($this->user->isWritable($user, $file)){                if (chmod($file, octdec($chmod[1]))){                    $this->send($fd, "200 Permissions changed on {$chmod[2]}");                    $this->log($user."\tCHMOD: $file to {$chmod[1]}",'info');                }else{                    $this->send($fd, "550 Could not change perms on " . $chmod[2] . ": Permission denied");                }            }else{                $this->send($fd, "550 You're unauthorized: Permission denied");            }        }else{            $this->send($fd, "500 Unknown Command");        }    }                        /**     * 更改传输类型     * @param $fd     * @param $data     */    public function cmd_TYPE($fd, $data){        switch ($data){            case "A":                $type = "ASCII";                break;            case "I":                $type = "8-bit binary";                break;        }        $this->send($fd, "200 TYPE is now " . $type);    }        /**     * 遍历目录     * @param $fd     * @param $data     */    public function cmd_LIST($fd, $data){        $user = $this->getUser($fd);        $ftpsock = $this->getUserSock($user);        if (!$ftpsock){            $this->send($fd, "425 Connection Error");            return;        }                $path = $this->joinPath($this->getAbsDir($user),'/');        $this->send($fd, "150 Opening ASCII mode data connection for file list");        $filelist = $this->getFileList($user, $path, true);        fwrite($ftpsock, $filelist);                    $this->send($fd, "226 Transfer complete.");                $this->closeUserSock($user);    }        /**     * 建立数据传输通     * @param $fd     * @param $data     */// 不使用主动模式    //     public function cmd_PORT($fd, $data){//         $user = $this->getUser($fd);//         $port = explode(",", $data);//         if (count($port) != 6){//             $this->send($fd, "501 Syntax error in IP address");//         }else{//             if (!$this->isIPAddress($port)){//                 $this->send($fd, "501 Syntax error in IP address");//                 return;//             }//             $ip = $port[0] . "." . $port[1] . "." . $port[2] . "." . $port[3];//             $port = hexdec(dechex($port[4]) . dechex($port[5]));//             if ($port < 1024){//                 $this->send($fd, "501 Sorry, but I won't connect to ports < 1024");//             }elseif ($port > 65000){//                 $this->send($fd, "501 Sorry, but I won't connect to ports > 65000");//             }else{            //                 $ftpsock = fsockopen($ip, $port);                                //                 if ($ftpsock){//                     $this->session[$user]['sock'] = $ftpsock;//                     $this->session[$user]['pasv'] = false;                                            //                     $this->send($fd, "200 PORT command successful");                        //                 }else{//                     $this->send($fd, "501 Connection failed");//                 }//             }//         }//     }                /**     * 被动模式         * @param unknown $fd     * @param unknown $data     */    public function cmd_PASV($fd, $data){        $user = $this->getUser($fd);        $ssl = false;        $pasv_port = $this->getPasvPort();        if($this->connection[$fd]['ssl'] === true){            $ssl = true;            $context = stream_context_create();                        // local_cert must be in PEM format            stream_context_set_option($context, 'ssl', 'local_cert', $this->setting['ssl_cert_file']);            // Path to local private key file             stream_context_set_option($context, 'ssl', 'local_pk', $this->setting['ssl_key_file']);                        stream_context_set_option($context, 'ssl', 'allow_self_signed', true);            stream_context_set_option($context, 'ssl', 'verify_peer', false);            stream_context_set_option($context, 'ssl', 'verify_peer_name', false);                stream_context_set_option($context, 'ssl', 'passphrase', '');                        // Create the server socket            $sock = stream_socket_server('ssl://0.0.0.0:'.$pasv_port, $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $context);        }else{            $sock = stream_socket_server('tcp://0.0.0.0:'.$pasv_port, $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN);        }        if ($sock){            $addr = stream_socket_get_name($sock, false);            list($ip, $port) = explode(':', $addr);            $ipArr = swoole_get_local_ip();            foreach($ipArr as $nic => $addr){                $ip = $addr;            }            $this->log("ServerSock: $ip:$port");            $ip = str_replace('.', ',', $ip);            $this->send($fd, "227 Entering Passive Mode ({$ip},".(intval($port) >> 8 & 0xff).",".(intval($port) & 0xff)."). ".$port." ".($ssl?'ssl':''));            $this->session[$user]['serv_sock'] = $sock;            $this->session[$user]['pasv'] = true;            $this->pushPasvPort($port);        }else{            fclose($sock);            $this->send($fd, "500 failed to create data socket: ".$errstr);        }    }                public function cmd_NOOP($fd,$data){        $this->send($fd, "200 OK");    }        //==================    //RFC2228    //==================        public function cmd_PBSZ($fd,$data){        $this->send($fd, '200 Command okay.');    }        public function cmd_PROT($fd,$data){        if(trim($data) == 'P'){            $this->connection[$fd]['ssl'] = true;            $this->send($fd, '200 Set Private level on data connection.');        }elseif(trim($data) == 'C'){            $this->connection[$fd]['ssl'] = false;            $this->send($fd, '200 Set Clear level on data connection.');        }else{            $this->send($fd, '504 Command not implemented for that parameter.');        }            }        //==================    //RFC2389    //==================    public function cmd_FEAT($fd,$data){        $this->send($fd, '211-Features supported');        $this->send($fd, 'MDTM');        $this->send($fd, 'SIZE');        $this->send($fd, 'SITE CHMOD');        $this->send($fd, 'REST STREAM');        $this->send($fd, 'MLSD Type*;Size*;Modify*;UNIX.mode*;');        $this->send($fd, 'PBSZ');        $this->send($fd, 'PROT');        $this->send($fd, '211 End');    }    //关闭utf8对中文文件名有影响    public function cmd_OPTS($fd,$data){        $this->send($fd, '502 Command not implemented.');    }            //==================    //RFC3659    //==================    /**     * 获取文件修改时间     * @param unknown $fd     * @param unknown $data     */    public function cmd_MDTM($fd,$data){        $user = $this->getUser($fd);        if (($file = $this->getFile($user, $data)) != false){            $this->send($fd, '213 '.date('YmdHis.u',filemtime($file)));        }else{            $this->send($fd, '550 No file named "'.$data.'"');        }    }        /**     * 获取文件大小     * @param $fd     * @param $data     */    public function cmd_SIZE($fd,$data){        $user = $this->getUser($fd);        if (($file = $this->getFile($user, $data)) != false){            $this->send($fd, '213 '.filesize($file));        }else{            $this->send($fd, '550 No file named "'.$data.'"');        }    }        /**     * 获取文件列表     * @param unknown $fd     * @param unknown $data     */    public function cmd_MLSD($fd,$data){        $user = $this->getUser($fd);        $ftpsock = $this->getUserSock($user);        if (!$ftpsock){            $this->send($fd, "425 Connection Error");            return;        }        $path = $this->joinPath($this->getAbsDir($user),'/');        $this->send($fd, "150 Opening ASCII mode data connection for file list");        $filelist = $this->getFileList($user, $path, true,'mlsd');        fwrite($ftpsock, $filelist);        $this->send($fd, "226 Transfer complete.");        $this->closeUserSock($user);    }        /**     * 设置文件offset     * @param unknown $fd     * @param unknown $data     */    public function cmd_REST($fd,$data){        $user = $this->getUser($fd);        $data= preg_replace('/[^0-9]/', '', $data);        if($data != ''){            $this->session[$user]['rest_offset'] = $data;            $this->send($fd, '350 Restarting at '.$data.'. Send STOR or RETR');        }else{            $this->send($fd, '500 Syntax error, offset unrecognized.');        }    }        /**     * 获取文件hash值     * @param unknown $fd     * @param unknown $data     */    public function cmd_HASH($fd,$data){        $user = $this->getUser($fd);        $ftpsock = $this->getUserSock($user);        if (($file = $this->getFile($user, $data)) != false){            if(is_file($file)){                $algo = 'sha512';                $this->send($fd, "200 ".hash_file($algo, $file));            }else{                $this->send($fd, "550 Can't open " . $data . ": No such file。");            }                    }else{            $this->send($fd, "550 Can't open " . $data . ": No such file。");        }    }        /**     * 控制台命令     * @param unknown $fd     * @param unknown $data     */    public function cmd_CONSOLE($fd,$data){        $group = $this->user->getUserProfile($this->getUser($fd));        $group = $group['group'];        if($group != 'admin'){            $this->send($fd, "550 You're unauthorized: Permission denied");            return;        }                $data = explode('||', $data);        $cmd = strtoupper($data[0]);        switch ($cmd){                        case 'USER-ONLINE':                $shm_data = $this->shm->read();                            $list = array();                if($shm_data !== false){                    if(isset($shm_data['online'])){                        $list = $shm_data['online'];                    }                                }                $this->send($fd, '200 '.json_encode($list));                break;                                        //Format: user-add||{"user":"","pass":"","home":"","expired":"","active":boolean,"group":"","description":"","email":""}            case 'USER-ADD':                if(isset($data[1])){                    $json = json_decode(trim($data[1]),true);                    $user = isset($json['user'])?$json['user']:'';                    $pass = isset($json['pass'])?$json['pass']:'';                    $home = isset($json['home'])?$json['home']:'';                    $expired = isset($json['expired'])?$json['expired']:'1999-01-01';                    $active = isset($json['active'])?$json['active']:false;                    $group = isset($json['group'])?$json['group']:'';                    $description = isset($json['description'])?$json['description']:'';                    $email = isset($json['email'])?$json['email']:'';                    if($this->user->addUser($user,$pass,$home,$expired,$active,$group,$description,$email)){                        $this->user->save();                        $this->user->reload();                        $this->send($fd, '200 User "'.$user.'" added.');                    }else{                        $this->send($fd, '550 Add fail!');                    }                }else{                    $this->send($fd, '500 Syntax error: USER-ADD||{"user":"","pass":"","home":"","expired":"","active":boolean,"group":"","description":""}');                }                break;                                //Format: user-set-profile||{"user":"","profile":[]}            case 'USER-SET-PROFILE':                if(isset($data[1])){                    $json = json_decode(trim($data[1]),true);                    $user = isset($json['user'])?$json['user']:'';                    $profile = isset($json['profile'])?$json['profile']:array();                                        if($this->user->setUserProfile($user, $profile)){                        $this->user->save();                        $this->user->reload();                        $this->send($fd, '200 User "'.$user.'" profile changed.');                    }else{                        $this->send($fd, '550 Set profile fail!');                    }                }else{                    $this->send($fd, '500 Syntax error: USER-SET-PROFILE||{"user":"","profile":[]}');                }                                break;                    //Format: user-get-profile||{"user":""}            case 'USER-GET-PROFILE':                if(isset($data[1])){                    $json = json_decode(trim($data[1]),true);                    $user = isset($json['user'])?$json['user']:'';                    $this->user->reload();                    if($profile = $this->user->getUserProfile($user)){                                                $this->send($fd, '200 '.json_encode($profile));                    }else{                        $this->send($fd, '550 Get profile fail!');                    }                }else{                    $this->send($fd, '500 Syntax error: USER-GET-PROFILE||{"user":""}');                }                                break;                //Format: user-delete||{"user":""}            case 'USER-DELETE':                if(isset($data[1])){                    $json = json_decode(trim($data[1]),true);                    $user = isset($json['user'])?$json['user']:'';                    if($this->user->delUser($user)){                        $this->user->save();                        $this->user->reload();                        $this->send($fd, '200 User '.$user.' deleted.');                    }else{                        $this->send($fd, '550 Delete user fail!');                    }                }else{                    $this->send($fd, '500 Syntax error: USER-DELETE||{"user":""}');                }                break;            case 'USER-LIST':                $this->user->reload();                $list = $this->user->getUserList();                $this->send($fd, '200 '.json_encode($list));                break;                                //Format: group-add||{"group":"","home":""}            case 'GROUP-ADD':                if(isset($data[1])){                    $json = json_decode(trim($data[1]),true);                    $group = isset($json['group'])?$json['group']:'';                                        $home = isset($json['home'])?$json['home']:'';                                        if($this->user->addGroup($group, $home)){                        $this->user->save();                        $this->user->reload();                        $this->send($fd, '200 Group "'.$group.'" added.');                    }else{                        $this->send($fd, '550 Add group fail!');                    }                }else{                    $this->send($fd, '500 Syntax error: GROUP-ADD||{"group":"","home":""}');                }                break;                            //Format: group-set-profile||{"group":"","profile":[]}            case 'GROUP-SET-PROFILE':                if(isset($data[1])){                    $json = json_decode(trim($data[1]),true);                    $group = isset($json['group'])?$json['group']:'';                    $profile = isset($json['profile'])?$json['profile']:array();                    if($this->user->setGroupProfile($group, $profile)){                        $this->user->save();                        $this->user->reload();                        $this->send($fd, '200 Group "'.$group.'" profile changed.');                    }else{                        $this->send($fd, '550 Set profile fail!');                    }                }else{                    $this->send($fd, '500 Syntax error: GROUP-SET-PROFILE||{"group":"","profile":[]}');                }                break;                                //Format: group-get-profile||{"group":""}            case 'GROUP-GET-PROFILE':                if(isset($data[1])){                    $json = json_decode(trim($data[1]),true);                    $group = isset($json['group'])?$json['group']:'';                    $this->user->reload();                    if($profile = $this->user->getGroupProfile($group)){                        $this->send($fd, '200 '.json_encode($profile));                    }else{                        $this->send($fd, '550 Get profile fail!');                    }                }else{                    $this->send($fd, '500 Syntax error: GROUP-GET-PROFILE||{"group":""}');                }                break;            //Format: group-delete||{"group":""}            case 'GROUP-DELETE':                if(isset($data[1])){                    $json = json_decode(trim($data[1]),true);                    $group = isset($json['group'])?$json['group']:'';                    if($this->user->delGroup($group)){                        $this->user->save();                        $this->user->reload();                        $this->send($fd, '200 Group '.$group.' deleted.');                    }else{                        $this->send($fd, '550 Delete group fail!');                    }                }else{                    $this->send($fd, '500 Syntax error: GROUP-DELETE||{"group":""}');                }                break;                            case 'GROUP-LIST':                $this->user->reload();                $list = $this->user->getGroupList();                $this->send($fd, '200 '.json_encode($list));                break;                //获取组用户列表                //Format: group-user-list||{"group":""}            case 'GROUP-USER-LIST':                if(isset($data[1])){                    $json = json_decode(trim($data[1]),true);                    $group = isset($json['group'])?$json['group']:'';                    $this->user->reload();                    $this->send($fd, '200 '.json_encode($this->user->getUserListOfGroup($group)));                }else{                    $this->send($fd, '500 Syntax error: GROUP-USER-LIST||{"group":""}');                }                break;                // 获取磁盘空间                //Format: disk-total||{"path":""}            case 'DISK-TOTAL':                if(isset($data[1])){                    $json = json_decode(trim($data[1]),true);                    $path = isset($json['path'])?$json['path']:'';                    $size = 0;                    if($path){                        $size = disk_total_space($path);                    }                    $this->send($fd, '200 '.$size);                }else{                    $this->send($fd, '500 Syntax error: DISK-TOTAL||{"path":""}');                }                break;                // 获取磁盘空间                //Format: disk-total||{"path":""}            case 'DISK-FREE':                if(isset($data[1])){                    $json = json_decode(trim($data[1]),true);                    $path = isset($json['path'])?$json['path']:'';                    $size = 0;                    if($path){                        $size = disk_free_space($path);                    }                    $this->send($fd, '200 '.$size);                }else{                    $this->send($fd, '500 Syntax error: DISK-FREE||{"path":""}');                }                break;            case 'HELP':                $list = 'USER-ONLINE USER-ADD USER-SET-PROFILE USER-GET-PROFILE USER-DELETE USER-LIST GROUP-ADD GROUP-SET-PROFILE GROUP-GET-PROFILE GROUP-DELETE GROUP-LIST GROUP-USER-LIST DISK-TOTAL DISK-FREE';                $this->send($fd, '200 '.$list);                break;            default:                $this->send($fd, '500 Syntax error.');        }    }        }

复制代码

 

 

总结:

至此,我们就可以实现一个完整的ftp服务器了。这个服务器的功能可以进行完全个性化定制。

下载仅供下载体验和测试学习,不得商用和正当使用。

下载体验

请输入密码查看内容!

如何获取密码?

 

点击下载