來源: http://dev.yesky.com/web/263/2638263.shtml
摘要:在本文中,讓我們共同探討基於PHP語言構建一個基本的伺服器端監視引擎的諸多技巧及注意事項,並給出完整的源碼實現。
一. 更改工作目錄的問題
當你編寫一個監視程式時,讓它設定自己的工作目錄通常更好些。這樣以來,如果你使用一個相對路徑讀寫檔案,那麼,它會根據情況自動處理使用者期望存放檔案的位置。總是限制程式中使用的路徑儘管是一種良好的實踐;但是,卻失去了應有的靈活性。因此,改變你的工作目錄的最安全的方法是,既使用chdir()也使用chroot()。
chroot()可用於PHP的CLI和CGI版本中,但是卻要求程式以根許可權運行。chroot()實際上把當前進程的路徑從根目錄改變到指定的目錄。這使得當前進程只能執行存在於該目錄下的檔案。經常情況下,chroot()由伺服器作為一個"安全裝置"使用以確保惡意代碼不會修改一個特定的目錄之外的檔案。請牢記,儘管chroot()能夠阻止你訪問你的新目錄之外的任何檔案,但是,任何當前開啟的檔案資源仍然能夠被存取。例如,下列代碼能夠開啟一個記錄檔,調用chroot()並切換到一個資料目錄;然後,仍然能夠成功地登入並進而開啟檔案資源:
<?php $logfile = fopen("/var/log/chroot.log", "w"); chroot("/Users/george"); fputs($logfile, "Hello From Inside The Chroot/n"); ?> |
如果一個應用程式不能使用chroot(),那麼你可以調用chdir()來設定工作目錄。例如,當代碼需要載入特定的代碼(這些代碼能夠在系統的任何地方被定位時),這是很有用的。注意,chdir()沒有提供安全機制來防止開啟未授權的檔案。
二. 放棄特權
當編寫Unix精靈時,一種經典的安全預防措施是讓它們放棄所有不需要的特權;否則,擁有不需要的特權容易招致不必要的麻煩。在代碼(或PHP本身)中含有漏洞的情況下,通過確保一個精靈以最小許可權使用者身份運行,往往能夠使損失減到最小。
一種實現此目的的方法是,以非特權使用者身份執行該精靈。然而,如果程式需要在一開始就開啟非特權使用者無權開啟的資源(例如記錄檔,資料檔案,通訊端,等等)的話,這通常是不夠的。
如果你以根使用者身份運行,那麼你能夠藉助於posix_setuid()和posiz_setgid()函數來放棄你的特權。下面的樣本把當前運行程式的特權改變為使用者nobody所擁有的那些許可權:
$pw=posix_getpwnam('nobody'); posix_setuid($pw['uid']); posix_setgid($pw['gid']); |
就象chroot()一樣,任何在放棄特權之前被開啟的特權資源都會保持為開啟,但是不能建立新的資源。
三. 保證排它性
你可能經常想實現:一個指令碼在任何時刻僅運行一個執行個體。為了保護指令碼,這是特別重要的,因為在後台運行容易導致偶然情況下調用多個執行個體。
保證這種排它性的標準技術是,通過使用flock()來讓指令碼鎖定一個特定的檔案(經常是一個加鎖檔案,並且被排它式使用)。如果鎖定失敗,該指令碼應該輸出一個錯誤並退出。下面是一個樣本:
$fp=fopen("/tmp/.lockfile","a"); if(!$fp || !flock($fp, LOCK_EX | LOCK_NB)) { fputs(STDERR, "Failed to acquire lock/n"); exit; } /*成功鎖定以安全地執行工作*/ |
注意,有關鎖機制的討論涉及較多內容,在此不多加解釋。
四. 構建監視服務
在這一節中,我們將使用PHP來編寫一個基本的監視引擎。因為你不會事Crowdsourced Security Testing道怎樣改變,所以你應該使它的實現既靈活又具可能性。
該記錄程式應該能夠支援任意的服務檢查(例如,HTTP和FTP服務)並且能夠以任意方式(通過電子郵件,輸出到一個記錄檔,等等)記錄事件。你當然想讓它以一個精靈方式運行;所以,你應該請求它輸出其完整的目前狀態。
一個服務需要實現下列抽象類別:
abstract class ServiceCheck { const FAILURE = 0; const SUCCESS = 1; protected $timeout = 30; protected $next_attempt; protected $current_status = ServiceCheck::SUCCESS; protected $previous_status = ServiceCheck::SUCCESS; protected $frequency = 30; protected $description; protected $consecutive_failures = 0; protected $status_time; protected $failure_time; protected $loggers = array(); abstract public function __construct($params); public function __call($name, $args) { if(isset($this->$name)) { return $this->$name; } } public function set_next_attempt() { $this->next_attempt = time() + $this->frequency; } public abstract function run(); public function post_run($status) { if($status !== $this->current_status) { $this->previous_status = $this->current_status; } if($status === self::FAILURE) { if( $this->current_status === self::FAILURE ) { $this->consecutive_failures++; } else { $this->failure_time = time(); } } else { $this->consecutive_failures = 0; } $this->status_time = time(); $this->current_status = $status; $this->log_service_event(); } public function log_current_status() { foreach($this->loggers as $logger) { $logger->log_current_status($this); } } private function log_service_event() { foreach($this->loggers as $logger) { $logger->log_service_event($this); } } public function register_logger(ServiceLogger $logger) { $this->loggers[] = $logger; } } |
上面的__call()重載方法提供對一個ServiceCheck對象的參數的唯讀存取操作:
· timeout-在引擎終止檢查之前,這一檢查能夠掛起多長時間。
· next_attempt-下次嘗試串連到伺服器的時間。
· current_status-服務的目前狀態:SUCCESS或FAILURE。
· previous_status-目前狀態之前的狀態。
· frequency-每隔多長時間檢查一次服務。
· description-服務描述。
· consecutive_failures-自從上次成功以來,服務檢查連續失敗的次數。
· status_time-服務被檢查的最後時間。
· failure_time-如果狀態為FAILED,則它代表發生失敗的時間。
這個類還實現了觀察者模式,允許ServiceLogger類型的對象註冊自身,然後當調用log_current_status()或log_service_event()時調用它。
這裡實現的關鍵函數是run(),它負責定義應該怎樣執行檢查。如果檢查成功,它應該返回SUCCESS;否則返回FAILURE。
當定義在run()中的服務檢查返回後,post_run()方法被調用。它負責設定對象的狀態並實現記入日誌。
ServiceLogger介面:指定一個日誌類僅需要實現兩個方法:log_service_event()和log_current_status(),它們分別在當一個run()檢查返回時和當實現一個普通狀態請求時被調用。
該介面如下所示:
interface ServiceLogger { public function log_service_event(ServiceCheck$service); public function log_current_status(ServiceCheck$service); } |
最後,你需要編寫引擎本身。該想法類似於在前一節編寫簡單程式時使用的思想:伺服器應該建立一個新的進程來處理每一次檢查並使用一個SIGCHLD處理器來檢測當檢查完成時的傳回值。可以同時檢查的最大數目應該是可配置的,從而可以防止對系統資源的過渡使用。所有的服務和日誌都將在一個XML檔案中定義。
下面是定義該引擎的ServiceCheckRunner類:
class ServiceCheckRunner { private $num_children; private $services = array(); private $children = array(); public function _ _construct($conf, $num_children) { $loggers = array(); $this->num_children = $num_children; $conf = simplexml_load_file($conf); foreach($conf->loggers->logger as $logger) { $class = new Reflection_Class("$logger->class"); if($class->isInstantiable()) { $loggers["$logger->id"] = $class->newInstance(); } else { fputs(STDERR, "{$logger->class} cannot be instantiated./n"); exit; } } foreach($conf->services->service as $service) { $class = new Reflection_Class("$service->class"); if($class->isInstantiable()) { $item = $class->newInstance($service->params); foreach($service->loggers->logger as $logger) { $item->register_logger($loggers["$logger"]); } $this->services[] = $item; } else { fputs(STDERR, "{$service->class} is not instantiable./n"); exit; } } } private function next_attempt_sort($a, $b){ if($a->next_attempt() == $b->next_attempt()) { return 0; } return ($a->next_attempt() < $b->next_attempt())? -1 : 1; } private function next(){ usort($this->services,array($this,'next_attempt_sort')); return $this->services[0]; } public function loop(){ declare(ticks=1); pcntl_signal(SIGCHLD, array($this, "sig_child")); pcntl_signal(SIGUSR1, array($this, "sig_usr1")); while(1) { $now = time(); if(count($this->children)< $this->num_children) { $service = $this->next(); if($now < $service->next_attempt()) { sleep(1); continue; } $service->set_next_attempt(); if($pid = pcntl_fork()) { $this->children[$pid] = $service; } else { pcntl_alarm($service->timeout()); exit($service->run()); } } } } public function log_current_status(){ foreach($this->services as $service) { $service->log_current_status(); } } private function sig_child($signal){ $status = ServiceCheck::FAILURE; pcntl_signal(SIGCHLD, array($this, "sig_child")); while(($pid = pcntl_wait($status, WNOHANG)) > 0){ $service = $this->children[$pid]; unset($this->children[$pid]); if(pcntl_wifexited($status) && pcntl_wexitstatus($status) ==ServiceCheck::SUCCESS) { $status = ServiceCheck::SUCCESS; } $service->post_run($status); } } private function sig_usr1($signal){ pcntl_signal(SIGUSR1, array($this, "sig_usr1")); $this->log_current_status(); } } |
這是一個很複雜的類。其構造器讀取並分析一個XML檔案,建立所有的將被監視的服務,並建立記錄它們的日誌程式。
loop()方法是該類中的主要方法。它佈建要求的訊號處理器並檢查是否能夠建立一個新的子進程。現在,如果下一個事件(以next_attempt時間CHUO排序)運行良好,那麼一個新的進程將被建立。在這個新的子進程內,發出一個警告以防止測試期間超出它的時限,然後執行由run()定義的測試。
還存在兩個訊號處理器:SIGCHLD處理器sig_child(),負責收集已終止的子進程並執行它們的服務的post_run()方法;SIGUSR1處理器sig_usr1(),簡單地調用所有登入的日誌程式的log_current_status()方法,這可以用於得到整個系統的目前狀態。
當然,這個監視架構並不沒有做任何實際的事情。但是首先,你需要檢查一個服務。下列這個類檢查是否你從一個HTTP伺服器取回一個"200 Server OK"響應:
class HTTP_ServiceCheck extends ServiceCheck{ public $url; public function _ _construct($params){ foreach($params as $k => $v) { $k = "$k"; $this->$k = "$v"; } } public function run(){ if(is_resource(@fopen($this->url, "r"))) { return ServiceCheck::SUCCESS; } else { return ServiceCheck::FAILURE; } } } |
與你以前構建的架構相比,這個服務極其簡單,在此恕不多描述。
五. 樣本ServiceLogger進程
下面是一個樣本ServiceLogger進程。當一個服務停用時,它負責把一個電子郵件發送給一個待命人員:
class EmailMe_ServiceLogger implements ServiceLogger { public function log_service_event(ServiceCheck$service) { if($service->current_status ==ServiceCheck::FAILURE) { $message = "Problem with{$service->description()}/r/n"; mail('oncall@example.com', 'Service Event',$message); if($service->consecutive_failures() > 5) { mail('oncall_backup@example.com', 'Service Event', $message); } } } public function log_current_status(ServiceCheck$service){ return; } } |
如果連續失敗五次,那麼該進程還把一個訊息發送到一個備份地址。注意,它並沒有實現一個有意義的log_current_status()方法。
無論何時象如下這樣改變一個服務的狀態,你都應該實現一個寫向PHP錯誤記錄檔的ServiceLogger進程:
class ErrorLog_ServiceLogger implements ServiceLogger { public function log_service_event(ServiceCheck$service) { if($service->current_status() !==$service->previous_status()) { if($service->current_status() ===ServiceCheck::FAILURE) { $status = 'DOWN'; } else { $status = 'UP'; } error_log("{$service->description()} changed status to $status"); } } public function log_current_status(ServiceCheck$service) { error_log("{$service->description()}: $status"); } } |
該log_current_status()方法意味著,如果進程發送一個SIGUSR1訊號,它將把其完整的目前狀態複製到你的PHP錯誤記錄檔中。
該引擎使用如下的一個設定檔:
<config> <loggers> <logger> <id>errorlog</id> <class>ErrorLog_ServiceLogger</class> </logger> <logger> <id>emailme</id> <class>EmailMe_ServiceLogger</class> </logger> </loggers> <services> <service> <class>HTTP_ServiceCheck</class> <params> <description>OmniTI HTTP Check</description> <url>http://www.omniti.com</url> <timeout>30</timeout> <frequency>900</frequency> </params> <loggers> <logger>errorlog</logger> <logger>emailme</logger> </loggers> </service> <service> <class>HTTP_ServiceCheck</class> <params> <description>Home Page HTTP Check</description> <url>http://www.schlossnagle.org/~george</url> <timeout>30</timeout> <frequency>3600</frequency> </params> <loggers> <logger>errorlog</logger> </loggers> </service> </services> </config> |
當傳遞這個XML檔案時,ServiceCheckRunner的構造器對於每一個指定的日誌執行個體化一個日誌記錄程式。然後,它相應於每一個指定的服務執行個體化一個ServiceCheck對象。
注意 該構造器使用Reflection_Class類來實現該服務和日誌類的內在檢查-在你試圖執行個體化它們之前。儘管這是不必要的,但是它很好地示範了PHP 5中新的反射(Reflection)API的使用。除了這些類以外,反射API還提供一些類來實現對PHP中幾乎任何內部實體(類,方法或函數)的內在檢查。
為了使用你構建的引擎,你仍然需要一些封裝代碼。監視程式應該會禁止你試圖兩次啟動它-你不需要對每一個事件建立兩份訊息。當然,該監視程式還應該接收包括下列選項在內的一些選項:
| 選項 |
描述 |
| [-f] |
引擎的設定檔的一個位置,預設是monitor.xml。 |
| [-n] |
引擎允許的子進程池的大小,預設是5。 |
| [-d] |
一個停用該引擎的守護功能的標誌。在你編寫一個把資訊輸出到stdout或stderr的調試ServiceLogger進程時,這是很有用的。 |
下面是最終的監視程式指令碼,它分析選項,保證排它性並且運行服務檢查:
require_once "Service.inc"; require_once "Console/Getopt.php"; $shortoptions = "n:f:d"; $default_opts = array('n' => 5, 'f' =>'monitor.xml'); $args = getOptions($default_opts, $shortoptions,null); $fp = fopen("/tmp/.lockfile", "a"); if(!$fp || !flock($fp, LOCK_EX | LOCK_NB)) { fputs($stderr, "Failed to acquire lock/n"); exit; } if(!$args['d']) { if(pcntl_fork()) { exit; } posix_setsid(); if(pcntl_fork()) { exit; } } fwrite($fp, getmypid()); fflush($fp); $engine = new ServiceCheckRunner($args['f'],$args['n']); $engine->loop(); |
注意,這個樣本使用了定製的getOptions()函數。
在編寫一個適當的設定檔後,你可以按如下方式啟動該指令碼:
> ./monitor.php -f /etc/monitor.xml
這可以保護並繼續監視直到機器被關掉或該指令碼被殺死。
這個指令碼相當複雜,但是仍然存在一些容易改進的地方,這些只好留給讀者作為練習之用:
· 添加一個重新分析設定檔的SIGHUP處理器以便你能夠在不啟動伺服器的情況下改變更配置置。
· 編寫一個能夠登入到一個資料庫的ServiceLogger以用於儲存查詢資料。
· 編寫一個Web前端程式以為整個監視系統提供一種良好的GUI。