在上篇文章给大家介绍了使用PHP如何实现高效安全的ftp服务器(一),感兴趣的朋友可以点击了解详情。接下来通过本篇文章给大家介绍使用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,...) * ) * ) * )
文件夹和文件的权限说明:
* 文件权限
* R读 : 允许用户读取(即下载)文件。该权限不允许用户列出目录内容,执行该操作需要列表权限。
* W写: 允许用户写入(即上传)文件。该权限不允许用户修改现有的文件,执行该操作需要追加权限。
* A追加: 允许用户向现有文件中追加数据。该权限通常用于使用户能够对部分上传的文件进行续传。
* N重命名: 允许用户重命名现有的文件。
* D删除: 允许用户删除文件。
*
* 目录权限
* L列表: 允许用户列出目录中包含的文件。
* C创建: 允许用户在目录中新建子目录。
* N重命名: 允许用户在目录中重命名现有子目录。
* D删除: 允许用户在目录中删除现有子目录。注意: 如果目录包含文件,用户要删除目录还需要具有删除文件权限。
*
* 子目录权限
* I继承: 允许所有子目录继承其父目录具有的相同权限。继承权限适用于大多数情况,但是如果访问必须受限于子文件夹,例如实施强制访问控制(Mandatory Access Control)时,则取消继承并为文件夹逐一授予权限。
*
实现代码如下:
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扩展即可。
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服务器运行的方式可能有所不同。代码是驻留内存的。
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 © 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配置。
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; c本文地址:https://www.stayed.cn/item/12272
转载请注明出处。
本站部分内容来源于网络,如侵犯到您的权益,请 联系我