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


Posted in PHP onDecember 30, 2015

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

1.实现用户类CUser。

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

用户文件格式:

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

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

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

实现代码如下:

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

2.共享内存操作类

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

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

3.内置的web服务器类

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

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

4.FTP主类

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

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

总结:

至此,我们就可以实现一个完整的ftp服务器了。这个服务器的功能可以进行完全个性化定制。如果您有好的建议,也可以留言给我,谢谢。

PHP 相关文章推荐
PHP网页游戏学习之Xnova(ogame)源码解读(四)
Jun 23 PHP
PHP生成唯一订单号的方法汇总
Apr 16 PHP
浅析PHP中Session可能会引起并发问题
Jul 23 PHP
PHP 实现的将图片转换为TXT
Oct 21 PHP
详解WordPress中的头像缓存和代理中的缓存更新方法
Mar 01 PHP
PHP strip_tags() 去字符串中的 HTML、XML 以及 PHP 标签的函数
May 22 PHP
Yii控制器中操作视图js的方法
Jul 04 PHP
php根据用户名和手机号查询是否存在手机号码
Feb 16 PHP
CodeIgniter框架验证码类库文件与用法示例
Mar 18 PHP
利用PHP判断是手机移动端还是PC端访问的函数示例
Dec 14 PHP
PHP结合Ffmpeg快速搭建流媒体服务的实践记录
Oct 31 PHP
laravel框架中视图的基本使用方法分析
Nov 23 PHP
php获取当前页面完整URL地址
Dec 30 #PHP
详解WordPress中添加和执行动作的函数使用方法
Dec 29 #PHP
详解WordPress中创建和添加过滤器的相关PHP函数
Dec 29 #PHP
yii,CI,yaf框架+smarty模板使用方法
Dec 29 #PHP
WordPress中自定义后台管理界面配色方案的小技巧
Dec 29 #PHP
在WordPress中实现发送http请求的相关函数解析
Dec 29 #PHP
在WordPress的文章编辑器中设置默认内容的方法
Dec 29 #PHP
You might like
php 向访客和爬虫显示不同的内容
2009/11/09 PHP
Yii框架在页面输出执行sql语句以方便调试的实现方法
2014/12/24 PHP
php中注册器模式类用法实例分析
2015/11/03 PHP
Yii数据模型中rules类验证器用法分析
2016/07/15 PHP
php操作路径的经典方法(必看篇)
2016/10/04 PHP
php实现跨域提交form表单的方法【2种方法】
2016/10/17 PHP
php框架知识点的整理和补充
2021/03/01 PHP
Javascript-Mozilla和IE中的一个函数直接量的问题分析
2007/08/12 Javascript
获取css样式表内样式的js函数currentStyle(IE),defaultView(FF)
2011/02/14 Javascript
JS实现随机数生成算法示例代码
2013/08/08 Javascript
鼠标选择动态改变网页背景颜色的JS代码
2013/12/10 Javascript
js/jquery判断浏览器的方法小结
2014/09/02 Javascript
PHP和NodeJs开发的应用如何共用Session
2015/04/16 NodeJs
Javascript实现div的toggle效果实例分析
2015/06/09 Javascript
jQuery实现图片轮播特效代码分享
2015/09/15 Javascript
基于JS实现的随机数字抽签实例
2016/12/08 Javascript
Javascript中数组去重与拍平的方法示例
2017/02/03 Javascript
ES6学习教程之对象的扩展详解
2017/05/02 Javascript
jQuery easyui datagird编辑行删除行功能的实现代码
2018/09/20 jQuery
js实现点击烟花特效
2020/10/14 Javascript
Python入门篇之对象类型
2014/10/17 Python
Python 正则表达式入门(初级篇)
2016/12/07 Python
django 创建过滤器的实例详解
2017/08/14 Python
python 常见字符串与函数的用法详解
2018/11/23 Python
python制作朋友圈九宫格图片
2019/11/03 Python
python tornado使用流生成图片的例子
2019/11/18 Python
selenium 多窗口切换的实现(windows)
2020/01/18 Python
python匿名函数lambda原理及实例解析
2020/02/07 Python
python中判断数字是否为质数的实例讲解
2020/12/06 Python
澳大利亚优质葡萄酒专家:Vintage Cellars
2019/01/08 全球购物
决心书范文
2014/03/11 职场文书
舞蹈兴趣小组活动总结
2014/07/07 职场文书
乡镇党员干部四风对照检查材料思想汇报
2014/09/27 职场文书
教师党员承诺书2015
2015/01/21 职场文书
golang http使用踩过的坑与填坑指南
2021/04/27 Golang
ubuntu如何搭建vsftpd服务器
2022/12/24 Servers