在上篇文章给大家介绍了使用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' " [debug]\tIP ACCESS:".' '.($pass"\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'])" [debug]\tACCESS:$access ".' '.($isExactMatch" $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'])"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]) "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'])"\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])"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"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'])"'.$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'])"'.$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'])"user":""}'); } break; //Format: user-delete||{"user":""} case 'USER-DELETE': if(isset($data[1])){ $json = json_decode(trim($data[1]),true); $user = isset($json['user'])"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'])"'.$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'])"'.$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'])"group":""}'); } break; //Format: group-delete||{"group":""} case 'GROUP-DELETE': if(isset($data[1])){ $json = json_decode(trim($data[1]),true); $group = isset($json['group'])"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'])"group":""}'); } break; // 获取磁盘空间 //Format: disk-total||{"path":""} case 'DISK-TOTAL': if(isset($data[1])){ $json = json_decode(trim($data[1]),true); $path = isset($json['path'])"path":""}'); } break; // 获取磁盘空间 //Format: disk-total||{"path":""} case 'DISK-FREE': if(isset($data[1])){ $json = json_decode(trim($data[1]),true); $path = isset($json['path'])"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_ftp服务器
稳了!魔兽国服回归的3条重磅消息!官宣时间再确认!
昨天有一位朋友在大神群里分享,自己亚服账号被封号之后居然弹出了国服的封号信息对话框。
这里面让他访问的是一个国服的战网网址,com.cn和后面的zh都非常明白地表明这就是国服战网。
而他在复制这个网址并且进行登录之后,确实是网易的网址,也就是我们熟悉的停服之后国服发布的暴雪游戏产品运营到期开放退款的说明。这是一件比较奇怪的事情,因为以前都没有出现这样的情况,现在突然提示跳转到国服战网的网址,是不是说明了简体中文客户端已经开始进行更新了呢?