在上篇文章给大家介绍了使用,感兴趣的朋友可以点击了解详情。接下来通过本篇文章给大家介绍使用PHP如何实现高效安全的ftp服务器(二),具体内容如下所示:
1.实现用户类CUser。
用户的存储采用文本形式,将用户数组进行json编码。
用户文件格式:
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,...)
* )
* )
* )
*
* 组文件格式:
* array(
* 'group1'=>array(
* 'home'=>'/home/ftp/dept1/',* 'folder'=>array(
*
* ),* 'deny'=>array(ip1,...)
* )
* )
* )
文件夹和文件的权限说明:
* 文件权限
* R读 : 允许用户读取(即下载)文件。该权限不允许用户列出目录内容,执行该操作需要列表权限。
* W写: 允许用户写入(即上传)文件。该权限不允许用户修改现有的文件,执行该操作需要追加权限。
* A追加: 允许用户向现有文件中追加数据。该权限通常用于使用户能够对部分上传的文件进行续传。
* N重命名: 允许用户重命名现有的文件。
* D删除: 允许用户删除文件。
*
* 目录权限
* L列表: 允许用户列出目录中包含的文件。
* C创建: 允许用户在目录中新建子目录。
* N重命名: 允许用户在目录中重命名现有子目录。
* D删除: 允许用户在目录中删除现有子目录。注意: 如果目录包含文件,用户要删除目录还需要具有删除文件权限。
*
* 子目录权限
* I继承: 允许所有子目录继承其父目录具有的相同权限。继承权限适用于大多数情况,但是如果访问必须受限于子文件夹,例如实施强制访问控制(Mandatory Access Control)时,则取消继承并为文件夹逐一授予权限。
*
实现代码如下:
<div class="jb51code">
<pre class="brush:php;">
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','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,$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('',$ip) && !empty($addr)){
$pass = false;
break;
}
}
}
}
if(isset($this->users[$user]['ip']['allow'])){
foreach ($this->users[$user]['ip']['allow'] as $addr){
$pattern = '/'.str_replace('',$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('*',$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);
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);
if($result['isExactMatch']){
return $result['access'][3] == 'N';
}else{
return $result['access'][3] == 'N' && $result['access'][9] == 'I';
}
}
public function isDeletable($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);
if($result['isExactMatch']){
return $result['access'][5] == 'L';
}else{
return $result['access'][5] == 'L' && $result['access'][9] == 'I';
}
}
public function isFolderCreatable($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);
if($result['isExactMatch']){
return $result['access'][7] == 'N';
}else{
return $result['access'][7] == 'N' && $result['access'][9] == 'I';
}
}
public function isFolderDeletable($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',$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')." $pathn";
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:,mixed>
*/
public function addAttempt(ShareMemory $shm,$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,$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扩展即可。
- 构造函数
/
public function __construct(){
$key = 'F';
$size = 10241024;
$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," ")) !== false)$str = substr($str,$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)." ";
if(strlen($str) > $this->shm_size) return false;
if(($shm_id = shmop_open($this->shm_key,$this->shm_size)) !== false){
$count = shmop_write($shm_id,1);
shmop_close($shm_id);
return $count;
}
return false;
}
/**
- 删除内存块,下次使用时将重新开辟内存块
- @return boolean
*/
public function delete(){
if(($shm_id = shmop_open($this->shm_key,$this->shm_size)) !== false){
$result = shmop_delete($shm_id);
shmop_close($shm_id);
return $result;
}
return false;
}
}
3.内置的web服务器类
这个主要是嵌入在ftp的http服务器类,功能不是很完善,进行ftp的管理还是可行的。不过需要注意的是,这个实现与apache等其他http服务器运行的方式可能有所不同。代码是驻留内存的。
<div class="jb51code">
<pre class="brush:php;">
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 = "rnrn";
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,$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();
}
/**
|