使用PHP如何实现高效安全的ftp服务器(二)

前端技术 2023/09/05 PHP

在上篇文章给大家介绍了使用PHP如何实现高效安全的ftp服务器(一),感兴趣的朋友可以点击了解详情。接下来通过本篇文章给大家介绍使用PHP如何实现高效安全的ftp服务器(二),具体内容如下所示:

1.实现用户类CUser。

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

用户文件格式:

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

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

* 文件权限
* R读 : 允许用户读取(即下载)文件。该权限不允许用户列出目录内容,执行该操作需要列表权限。
* W写: 允许用户写入(即上传)文件。该权限不允许用户修改现有的文件,执行该操作需要追加权限。
* A追加: 允许用户向现有文件中追加数据。该权限通常用于使用户能够对部分上传的文件进行续传。
* N重命名: 允许用户重命名现有的文件。
* D删除: 允许用户删除文件。
*
* 目录权限
* L列表: 允许用户列出目录中包含的文件。
* C创建: 允许用户在目录中新建子目录。
* N重命名: 允许用户在目录中重命名现有子目录。
* D删除: 允许用户在目录中删除现有子目录。注意: 如果目录包含文件,用户要删除目录还需要具有删除文件权限。
*
* 子目录权限
* I继承: 允许所有子目录继承其父目录具有的相同权限。继承权限适用于大多数情况,但是如果访问必须受限于子文件夹,例如实施强制访问控制(Mandatory Access Control)时,则取消继承并为文件夹逐一授予权限。
*

  实现代码如下:  

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

2.共享内存操作类

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

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

3.内置的web服务器类

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

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

4.FTP主类

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

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

本文地址:https://www.stayed.cn/item/12272

转载请注明出处。

本站部分内容来源于网络,如侵犯到您的权益,请 联系我

我的博客

人生若只如初见,何事秋风悲画扇。