时间:2021-07-01 10:21:17 帮助过:51人阅读
在上篇文章给大家介绍了使用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; 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 sock