在上篇文章给大家介绍了使用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配置。
定義済み('DEBUG_ON') または定義('DEBUG_ON', false); //メインディレクトリ 定義済み('BASE_PATH') または定義('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'; クラス CFtpServer{ //ソフトウェアバージョン const バージョン = '2.0'; const EOF = "rn"; public static $software "FTP サーバー"; プライベート静的 $server_mode = SWOOLE_PROCESS; プライベート静的 $pid_file; プライベート静的 $log_file; //ファイルに書き込まれるログキュー(バッファ) プライベート $queue = array(); プライベート $pasv_port_range = 配列(55000,60000); パブリック $host = '0.0.0.0'; パブリック $ポート = 21; パブリック $setting = array(); //最大接続数 パブリック $max_connection = 50; //Web管理ポート パブリック $manager_port = 8080; //tls パブリック $ftps_port = 990; /*** @var swoole_server*/ 保護された $server; 保護された$connection = array(); 保護された $session = array(); protected $user;//ユーザー クラス、コピー検証と権限 //共有メモリクラス protected $shm;//ShareMemory /*** * @var 埋め込み http サーバー*/ 保護された $webserver; /*++++++++++++++++++++++++++++++++++++++++++++++ ++++++ + 静的メソッド ++++++++++++++++++++++++++++++++++++++++++++++++ ++++*/ パブリック静的関数 setPidFile($pid_file){ self::$pid_file = $pid_file; } /**※サービス起動制御方法*/ パブリック静的関数 start($startFunc){ if(empty(self::$pid_file)){ exit("pid ファイル.n が必要"); } if(!extension_loaded('posix')){ exit("拡張子 `posix`.n が必要です"); } if(!extension_loaded('swoole')){ exit("拡張子 `swoole`.n が必要です"); } if(!extension_loaded('shmop')){ exit("拡張子 `shmop`.n が必要です"); } if(!extension_loaded('openssl')){ exit("拡張子 `openssl`.n が必要です"); } $pid_file = self::$pid_file; $server_pid = 0; if(is_file($pid_file)){ $server_pid = file_get_contents($pid_file); } グローバル $argv; if(empty($argv[1])){ goto の使用法。 }elseif($argv[1] == 'リロード'){ if (空($server_pid)){ exit("FtpServer が実行されていませんn"); } posix_kill($server_pid, SIGUSR1); 出口; }elseif ($argv[1] == '停止'){ if (空($server_pid)){ exit("FtpServer が実行されていませんn"); } posix_kill($server_pid, SIGTERM); 出口; }elseif ($argv[1] == '開始'){ //ServerPID はすでに存在しており、プロセスも存在します if (!empty($server_pid) および posix_kill($server_pid,(int) 0)){ exit("FtpServer はすでに実行されています。n"); } //サーバーを起動します $startFunc(); }それ以外{ 使用法: exit("使用法: php {$argv[0]} start|stop|reloadn"); } } /*++++++++++++++++++++++++++++++++++++++++++++++ ++++++ +メソッド ++++++++++++++++++++++++++++++++++++++++++++++++ ++++*/ パブリック関数 __construct($host,$port){ $this->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( 「バックログ」 => 128、 'ディスパッチモード' => ); } パブリック関数 daemonize(){ $this->setting['daemonize'] = 1; } パブリック関数 getConnectionInfo($fd){ return $this->server->connection_info($fd); } /*** サービスプロセスを開始します * @param 配列 $setting * @throwsException*/ パブリック関数 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] 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(); } パブリック関数 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(ファイルサイズ($debug_file) > 10485760){//10M unlink($debug_file); } } $log をエコーします。 } if($level != 'デバッグ'){ //日志记录 $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(); } } パブリック関数 shutdown(){ return $this->server->shutdown(); } パブリック関数 close($fd){ return $this->server->close($fd); } パブリック関数 send($fd,$data){ $data = strtr($data,array("n" => "", " " => "", "r" => "")); $this->log("[-->]t" . $data); return $this->server->send($fd,$data.self::EOF); } /*++++++++++++++++++++++++++++++++++++++++++++++ ++++++++ + イベント回调 ++++++++++++++++++++++++++++++++++++++++++++++++ ++++++*/ パブリック関数 onMasterStart($serv){ グローバル $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('マスターが開始されました。'); } パブリック関数 onMasterStop($serv){ if (!empty($this->setting['pid_file'])){ unlink(self::$pid_file); } $this->shm->delete(); $this->log('マスターストップ。'); } パブリック関数 onManagerStart($serv){ グローバル $argv; swoole_set_process_name('php '.$argv[0].': マネージャー'); $this->log('マネージャーが開始されました。'); } パブリック関数 onManagerStop($serv){ $this->log('マネージャーは停止します。'); } パブリック関数 onWorkerStart($serv,$worker_id){ グローバル $argv; if($worker_id >= $serv->setting['worker_num']) { swoole_set_process_name("php {$argv[0]}: ワーカー [タスク]"); } それ以外 { swoole_set_process_name("php {$argv[0]}: ワーカー [{$worker_id}]"); } $this->log("ワーカー {$worker_id} が開始されました。"); } パブリック関数 onWorkerStop($serv,$worker_id){ $this->log("ワーカー {$worker_id} が停止します。"); } public function onWorkerError($serv,$worker_id,$worker_pid,$exit_code){ $this->log("ワーカー {$worker_id} エラー:{$exit_code}."); } パブリック関数 onConnect($serv,$fd,$from_id){ $info = $this->getConnectionInfo($fd); if($info['server_port'] == $this->manager_port){ //ウェブリクエスト $this->webserver = new CWebServer(); }それ以外{ $this->send($fd, "220---------- " . self::$software . " ----------"); $this->send($fd, "220-現地時間は現在 " . date("H:i")); $this->send($fd, "220 これはプライベート システムです - 匿名ログインはできません"); if(count($this->server->connections) max_connection){ if($info['server_port'] == $this->ポート && isset($this->setting['force_ssl']) && $this->setting['force_ssl']){ //如果启用强制ssl $this->send($fd, "421 TLS 経由の暗黙的な FTP が必要です。制御接続を閉じます。"); $this->close($fd); 戻る ; } $this->connection[$fd] = array(); $this->session = array(); $this->queue = array(); }それ以外{ $this->send($fd, "421 接続が多すぎます。制御接続を閉じています。"); $this->close($fd); } } } パブリック関数 onReceive($serv,$fd,$from_id,$recv_data){ $info = $this->getConnectionInfo($fd); if($info['server_port'] == $this->manager_port){ //ウェブリクエスト $this->webserver->onReceive($this->server, $fd, $recv_data); }それ以外{ $read = トリム($recv_data); $this->log("[send($fd, "500 不明なコマンド"); 戻る; } if (empty($this->connection[$fd]['login'])){ スイッチ($cmd[0]){ ケース「タイプ」: ケース「ユーザー」: ケース「パス」: 「終了」の場合: ケース「認証」: ケース「PBSZ」: 壊す; デフォルト: $this->send($fd,"530 ログインしていません"); 戻る; } } $this->$func($fd,$data); } } パブリック関数 onClose($serv,$fd,$from_id){ // 在中用 $shm_data = $this->shm->read(); if($shm_data !== false){ if(isset($shm_data['online'])){ $list = 配列(); 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('ソケット '.$fd.' を閉じます。ログをフラッシュします。','debug',true); } /*++++++++++++++++++++++++++++++++++++++++++++++ ++++++++ + ツール関数数 ++++++++++++++++++++++++++++++++++++++++++++++++ ++++++*/ /*** ユーザー名を取得する * @param $fd*/ パブリック関数 getUser($fd){ return isset($this->connection[$fd]['user'])?$this->connection[$fd]['user']:''; } /*** ファイルのフルパスを取得します * @param $user * @param $file * @return 文字列|ブール値*/ パブリック関数 getFile($user, $file){ $file = $this->fillDirName($user, $file); if (is_file($file)){ 実パス($file)を返します; }それ以外{ false を返します。 } } /*** ディレクトリを横断する * @param $rdir * @param $showHidden * @param $format list/mlsd * @戻り文字列 * * リストは現地時間を使用しています * 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)."rn"; } if ($handle = opendir($rdir)){ $isListable = $this->user->isFolderListable($user, $rdir); while (false !== ($file = readdir($handle))){ if ($file == '.' または $file == '..'){ 続く; } if ($file{0} == "." および !$showHidden){ 続く; } //現在の目录$rdirが列挙できない場合は、現在の目录の下の目录が列挙できるように構成されているかどうかを判断します if(!$isListable){ $dir = $rdir 。 $ファイル; if(is_dir($dir)){ $dir = $this->joinPath($dir, '/'); if($this->user->isFolderListable($user, $dir)){ リストフォルダーに移動します。 } } 続く; } リストフォルダー: $stats = stat($rdir . $file); if (is_dir($rdir . "/" . $file)) $mode = "d";それ以外の場合は $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."rn"; }それ以外{ $filelist.= 'Type=file;Size='.$stats['size'].';Modify='.gmdate('YmdHis',$stats['mtime']).';UNIX.mode='。 $モード。'; '.$file."rn"; } }それ以外{ $uidfill = ""; for ($i = strlen($stats['uid']); $i セッション[$user]['pwd']; if ($old_dir == $cdir){ $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; 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 * @戻り文字列*/ パブリック関数 fillDirName($user, $file){ if (substr($file, 0, 1) != "/"){ $file = '/'.$file; $file = $this->joinPath($this->getUserDir( $user), $file); } $file = $this->joinPath($this->セッション[$user]['home'],$file); $file を返します。 } /*** ユーザーパスを取得します * @param 不明な $user*/ パブリック関数 getUserDir($user){ return $this->session[$user]['pwd']; } /*** ユーザーの現在のファイル システムの絶対パス、非 chroot パスを取得します。 * @param $user * @戻り文字列*/ パブリック関数 getAbsDir($user){ $rdir = $this->joinPath($this->セッション[$user]['home'],$this->セッション[$user]['pwd']); $rdir を返します。 } /*** パス接続 * @パラメータ文字列 $path1 * @パラメータ文字列$path2 * @戻り文字列*/ パブリック関数 joinPath($path1,$path2){ $path1 = rtrim($path1,'/'); $path2 = トリム($path2,'/'); $path1.'/'.$path2 を返します。 } /**※知財判断 * @パラメータ文字列$ip * @return ブール値*/ パブリック関数 isIPAddress($ip){ if (!is_numeric($ip[0]) || $ip[0] 254) { false を返します。 elseif (!is_numeric($ip[1]) || $ip[1] 254) { false を返します。 elseif (!is_numeric($ip[2]) || $ip[2] 254) { false を返します。 elseif (!is_numeric($ip[3]) || $ip[3] 254) { false を返します。 elseif (!is_numeric($ip[4]) || $ip[4] 500) { false を返します。 elseif (!is_numeric($ip[5]) || $ip[5] 500) { false を返します。 } それ以外 { true を返します。 } } /*** pasv ポートの取得 * @戻り番号*/ パブリック関数 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 isAvailablePasvPort($port)){ 壊す; } $ループ++; } $ポートを返します。 } パブリック関数 PushPasvPort($port){ $shm_data = $this->shm->read(); if($shm_data !== false){ if(isset($shm_data['pasv_port'])){ array_push($shm_data['pasv_port'], $port); }それ以外{ $shm_data['pasv_port'] = 配列($port); } $this->shm->write($shm_data); $this->log('pasv ポートをプッシュ: '.implode(',', $shm_data['pasv_port'])); true を返します。 } false を返します。 } パブリック関数popPasvPort($port){ $shm_data = $this->shm->read(); if($shm_data !== false){ if(isset($shm_data['pasv_port'])){ $tmp = 配列(); 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'])); true を返します。 } false を返します。 } パブリック関数 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']); } true を返します。 } false を返します。 } /*** 現在のデータリンク TCP 数を取得します*/ パブリック関数 getDataConnections(){ $shm_data = $this->shm->read(); if($shm_data !== false){ if(isset($shm_data['pasv_port'])){ return count($shm_data['pasv_port']); } } 0を返します。 } /*** データ送信ソケットを閉じます * @param $user * @return bool*/ パブリック関数 closeUserSock($user){ $peer = stream_socket_get_name($this->セッション[$user]['sock'], false); list($ip,$port) =explode(':', $peer); //释放ポート占用 $this->popPasvPort($port); fclose($this->session[$user]['sock']); $this->セッション[$user]['sock'] = 0; true を返します。 } /*** @param $user * @return リソース*/ パブリック関数 getUserSock($user){ //被動作モード if ($this->session[$user]['pasv'] == true){ if (empty($this->session[$user]['sock'])){ $addr = stream_socket_get_name($this->セッション[$user]['serv_sock'], false); list($ip, $port) =explode(':', $addr); $sock = stream_socket_accept($this->セッション[$user]['serv_sock'], 5); if ($sock){ $peer = stream_socket_get_name($sock, true); $this->log("承認: 成功したクライアントは $peer です。"); $this->session[$user]['sock'] = $sock; //关闭サーバーソケット fclose($this->session[$user]['serv_sock']); }それ以外{ $this->log("受け入れ: 失敗しました。"); //释放口 $this->popPasvPort($port); false を返します。 } } } return $this->session[$user]['sock']; } /*++++++++++++++++++++++++++++++++++++++++++++++ ++++++++ + FTPコマンド ++++++++++++++++++++++++++++++++++++++++++++++++ ++++++*/ //================== //RFC959 //================== /*** ログインユーザー名 * @param $fd * @param $data*/ パブリック関数 cmd_USER($fd, $data){ if (preg_match("/^([a-z0-9.@]+)$/", $data)){ $user = strto lower($data); $this->connection[$fd]['user'] = $user; $this->send($fd, "331 ユーザー $user OK。パスワードが必要です"); }それ以外{ $this->send($fd, "530 ログイン認証に失敗しました"); } } /*** ログインパスワード * @param $fd * @param $data*/ パブリック関数 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 ログイン認証に失敗しました: ログイン試行が多すぎます。10 分以内にブロックされました。"); 戻る; } if ($this->user->checkUser($user, $pass, $ip)){ $dir = "/"; $this->セッション[$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 ログイン認証に失敗しました: `home` パス エラー。"); }それ以外{ $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。現在の制限されたディレクトリは " . $dir); $this->log('ユーザー '.$user .' は正常にログインしました! IP: '.$ip,'warn'); } }それ以外{ $this->user->addAttempt($this->shm, $user, $ip); $this->log('ユーザー '.$user .' ログイン失敗! IP: '.$ip,'warn'); $this->send($fd, "530 ログイン認証に失敗しました: パスまたは IP 許可ルールを確認してください。"); } } /*** 現在のディレクトリを変更します * @param $fd * @param $data*/ パブリック関数 cmd_CWD($fd, $data){ $user = $this->getUser($fd); if (($dir = $this->setUserDir($user, $data)) != false){ $this->send($fd, "250 OK。現在のディレクトリは " . $dir); }それ以外{ $this->send($fd, "550 ディレクトリを " . $data . " に変更できません: そのようなファイルまたはディレクトリはありません"); } } /**※上位ディレクトリに戻ります * @param $fd * @param $data*/ パブリック関数 cmd_CDUP($fd, $data){ $data = '..'; $this->cmd_CWD($fd, $data); } /*** サーバーを終了します * @param $fd * @param $data*/ パブリック関数 cmd_QUIT($fd, $data){ $this->send($fd,"221 さようなら。"); unset($this->connection[$fd]); } /*** 現在のディレクトリを取得します * @param $fd * @param $data*/ パブリック関数 cmd_PWD($fd, $data){ $user = $this->getUser($fd); $this->send($fd, "257 "" . $this->getUserDir($user) . "" は現在の場所です"); } /*** ダウンロードファイル * @param $fd * @param $data*/ パブリック関数 cmd_RETR($fd, $data){ $user = $this->getUser($fd); $ftpsock = $this->getUserSock($user); if (!$ftpsock){ $this->send($fd, "425 接続エラー"); 戻る; } if (($file = $this->getFile($user, $data)) != false){ if($this->user->isReadable($user, $file)){ $this->send($fd, "150 クライアントに接続中"); if ($fp = fopen($file, "rb")){ //断点续传 if(isset($this->session[$user]['rest_offset'])){ if(!fseek($fp, $this->session[$user]['rest_offset'])){ $this->log("オフセットでの RETR ".ftell($fp)); }それ以外{ $this->log("オフセット ".ftell($fp) での RETR。' 失敗しました。'); } unset($this->session[$user]['rest_offset']); } while (!feof($fp)){ $cont = fread($fp, 8192); if (!fwrite($ftpsock, $cont)) ブレーク; } if (fclose($fp) および $this->closeUserSock($user)){ $this->send($fd, "226 ファイルは正常に転送されました"); $this->log($user."tGET:".$file,'info'); }それ以外{ $this->send($fd, "ファイル転送中に 550 エラー"); } }それ以外{ $this->send($fd, "550 " . $data . " を開けません: アクセスが拒否されました"); } }それ以外{ $this->send($fd, "550 あなたは権限がありません: 許可が拒否されました"); } }それ以外{ $this->send($fd, "550 " . $data . " を開けません: そのようなファイルまたはディレクトリはありません"); } } /*** ファイルをアップロードする * @param $fd * @param $data*/ パブリック関数 cmd_STOR($fd, $data){ $user = $this->getUser($fd); $ftpsock = $this->getUserSock($user); if (!$ftpsock){ $this->send($fd, "425 接続エラー"); 戻る; } $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("STOR 用にオープンします。"); }それ以外{ $fp = fopen($file, 'wb'); $this->log("STOR 用に作成します。"); } if (!$fp){ $this->send($fd, "553 そのファイルを開けません: アクセス許可が拒否されました"); }それ以外{ //切断点续传,必要Append权限 if(isset($this->session[$user]['rest_offset'])){ if(!fseek($fp, $this->session[$user]['rest_offset'])){ $this->log("オフセットの STOR ".ftell($fp)); }それ以外{ $this->log("オフセット ".ftell($fp) の STOR。' 失敗しました。'); } unset($this->session[$user]['rest_offset']); } $this->send($fd, "150 クライアントに接続中"); while (!feof($ftpsock)){ $cont = fread($ftpsock, 8192); if (!$cont) ブレーク; if (!fwrite($fp, $cont)) ブレーク; } touch($file);//ファイルの保存期間と修正時間を設定します if (fclose($fp) および $this->closeUserSock($user)){ $this->send($fd, "226 ファイルは正常に転送されました"); $this->log($user."tPUT: $file",'info'); }それ以外{ $this->send($fd, "ファイル転送中に 550 エラー"); } } }それ以外{ $this->send($fd, "550 あなたは権限がありません: 許可が拒否されました"); $this->closeUserSock($user); } } /*** ファイルの追加 * @param $fd * @param $data*/ パブリック関数 cmd_APPE($fd,$data){ $user = $this->getUser($fd); $ftpsock = $this->getUserSock($user); if (!$ftpsock){ $this->send($fd, "425 接続エラー"); 戻る; } $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 そのファイルを開けません: アクセス許可が拒否されました"); }それ以外{ //切断点续传,必要Append权限 if(isset($this->session[$user]['rest_offset'])){ if(!fseek($fp, $this->session[$user]['rest_offset'])){ $this->log("オフセットでAPPE ".ftell($fp)); }それ以外{ $this->log("オフセット ".ftell($fp) での APPE。' 失敗しました。'); } unset($this->session[$user]['rest_offset']); } $this->send($fd, "150 クライアントに接続中"); while (!feof($ftpsock)){ $cont = fread($ftpsock, 8192); if (!$cont) ブレーク; if (!fwrite($fp, $cont)) ブレーク; } touch($file);//ファイルの保存期間と修正時間を設定します if (fclose($fp) および $this->closeUserSock($user)){ $this->send($fd, "226 ファイルは正常に転送されました"); $this->log($user."tAPPE: $file",'info'); }それ以外{ $this->send($fd, "ファイル転送中に 550 エラー"); } } }それ以外{ $this->send($fd, "550 あなたは権限がありません: 許可が拒否されました"); $this->closeUserSock($user); } } /*** ファイル名変更、ソースファイル * @param $fd * @param $data*/ パブリック関数 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 が受け入れられました - ファイルが存在し、宛先の準備ができています"); }それ以外{ $this->send($fd, "550 申し訳ありませんが、その '$data' は存在しません"); } } /*** ファイル名変更、対象ファイル * @param $fd * @param $data*/ パブリック関数 cmd_RNTO($fd, $data){ $user = $this->getUser($fd); $old_file = $this->セッション[$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) または !is_dir(dirname($new_file))){ $this->send($fd, "451 名前変更/移動失敗: そのようなファイルまたはディレクトリはありません"); }elseif (rename($old_file, $new_file)){ $this->send($fd, "250 ファイルは正常に名前変更または移動されました"); $this->log($user."tRENAME: $old_file から $new_file",'warn'); }それ以外{ $this->send($fd, "451 名前変更/移動失敗: 操作は許可されていません"); } }それ以外{ $this->send($fd, "550 あなたは権限がありません: 許可が拒否されました"); } unset($this->session[$user]['rename']); } /*** ファイルの削除 * @param $fd * @param $data*/ パブリック関数 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 " . $data . " を削除できませんでした: そのようなファイルまたはディレクトリはありません"); } elseif (unlink($file)){ $this->send($fd, "250 削除されました " . $data); $this->log($user."tDEL: $file",'warn'); }それ以外{ $this->send($fd, "550 " . $data . " を削除できませんでした: 権限が拒否されました"); } }それ以外{ $this->send($fd, "550 あなたは権限がありません: 許可が拒否されました"); } } /*** ディレクトリを作成します * @param $fd * @param $data*/ パブリック関数 cmd_MKD($fd, $data){ $user = $this->getUser($fd); $パス = ''; if($data[0] == '/'){ $path = $this->joinPath($this->session[$user]['home'],$data); }それ以外{ $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 ディレクトリを作成できません: そのようなファイルまたはディレクトリはありません"); }elseif(file_exists($path)){ $this->send($fd, "550 ディレクトリを作成できません: ファイルが存在します"); }それ以外{ if (mkdir($path)){ $this->send($fd, "257 "" . $data . "" : ディレクトリは正常に作成されました"); $this->log($user."tMKDIR: $path",'info'); }それ以外{ $this->send($fd, "550 ディレクトリを作成できません: アクセス許可が拒否されました"); } } }それ以外{ $this->send($fd, "550 あなたは権限がありません: 許可が拒否されました"); } } /*** ディレクトリの削除 * @param $fd * @param $data*/ パブリック関数 cmd_RMD($fd, $data){ $user = $this->getUser($fd); $dir = ''; if($data[0] == '/'){ $dir = $this->joinPath($this->session[$user]['home'], $data); }それ以外{ $dir = $this->fillDirName($user, $data); } $dir = $this->joinPath($dir, '/'); if($this->user->isFolderDeletable($user, $dir)){ if (is_dir(dirname($dir)) および is_dir($dir)){ if (count(glob($dir . "/*"))){ $this->send($fd, "550 ディレクトリを削除できません: ディレクトリが空ではありません"); }elseif (rmdir($dir)){ $this->send($fd, "250 ディレクトリは正常に削除されました"); $this->log($user."tRMDIR: $dir",'warn'); }それ以外{ $this->send($fd, "550 ディレクトリを削除できません: 操作は許可されていません"); } }elseif (is_dir(dirname($dir)) および file_exists($dir)){ $this->send($fd, "550 ディレクトリを削除できません: ディレクトリではありません"); }それ以外{ $this->send($fd, "550 ディレクトリを作成できません: そのようなファイルまたはディレクトリはありません"); } }それ以外{ $this->send($fd, "550 あなたは権限がありません: 許可が拒否されました"); } } /*** サーバーの種類を取得します * @param $fd * @param $data*/ パブリック関数 cmd_SYST($fd, $data){ $this->send($fd, "215 UNIX タイプ: L8"); } /*** 許可制御 * @param $fd * @param $data*/ パブリック関数 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, "{$chmod[2]} で 200 個のアクセス許可が変更されました"); $this->log($user."tCHMOD: $file から {$chmod[1]}",'info'); }それ以外{ $this->send($fd, "550 " . $chmod[2] . " のパーマを変更できませんでした: アクセスが拒否されました"); } }それ以外{ $this->send($fd, "550 あなたは権限がありません: 許可が拒否されました"); } }それ以外{ $this->send($fd, "500 不明なコマンド"); } } /*** 転送タイプの変更 * @param $fd * @param $data*/ パブリック関数 cmd_TYPE($fd, $data){ スイッチ ($data){ ケース「A」: $type = "ASCII"; 壊す; ケース「私」: $type = "8ビットバイナリ"; 壊す; } $this->send($fd, "200 TYPE は " . $type); } /*** ディレクトリを走査する * @param $fd * @param $data*/ パブリック関数 cmd_LIST($fd, $data){ $user = $this->getUser($fd); $ftpsock = $this->getUserSock($user); if (!$ftpsock){ $this->send($fd, "425 接続エラー"); 戻る; } $path = $this->joinPath($this->getAbsDir($user),'/'); $this->send($fd, "150 ファイル リストの ASCII モード データ接続を開く"); $filelist = $this->getFileList($user, $path, true); fwrite($ftpsock, $filelist); $this->send($fd, "226 転送が完了しました。"); $this->closeUserSock($user); } /*** データ伝送チャネルを確立します * @param $fd * @param $data*/ // 主アニメーションモードを使用しません // パブリック関数 cmd_PORT($fd, $data){ // $user = $this->getUser($fd); // $port =explode(",", $data); // if (count($port) != 6){ // $this->send($fd, "IP アドレスの 501 構文エラー"); // }それ以外{ // if (!$this->isIPAddress($port)){ // $this->send($fd, "IP アドレスの 501 構文エラー"); // 戻る; // } // $ip = $port[0] 。 「。」 。 $ポート[1] 。 「。」 。 $ポート[2] 。 「。」 。 $ポート[3]; // $port = hexdec(dechex($port[4]) . dechex($port[5])); // if ($port 65000){ // $this->send($fd, "501 申し訳ありませんが、ポート > 65000 には接続しません"); // }それ以外{ // $ftpsock = fsockopen($ip, $port); // if ($ftpsock){ // $this->session[$user]['sock'] = $ftpsock; // $this->session[$user]['pasv'] = false; // $this->send($fd, "200 PORT コマンドが成功しました"); // }それ以外{ // $this->send($fd, "501 接続に失敗しました"); // } // } // } // }/*** パッシブモード * @param 不明 $fd * @param 不明な $data*/ パブリック関数 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 は PEM 形式である必要があります stream_context_set_option($context, 'ssl', 'local_cert', $this->setting['ssl_cert_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', 'パスフレーズ', ''); // サーバーソケットを作成します $靴下 = st