Resumo: Neste artigo, vamos discutir as diversas técnicas e precauções para construir um mecanismo básico de monitoramento do lado do servidor baseado na linguagem PHP e fornecer uma implementação completa do código-fonte.
1. O problema de alterar o diretório de trabalho
Quando você escreve um programa de monitoramento, geralmente é melhor deixá-lo definir seu próprio diretório de trabalho. Dessa forma, se você usar um caminho relativo para ler e gravar arquivos, ele tratará automaticamente o local onde o usuário espera que o arquivo seja armazenado com base na situação. Embora seja uma boa prática limitar sempre os caminhos utilizados num programa, perde-se a flexibilidade que merece; Portanto, a maneira mais segura de alterar seu diretório de trabalho é usar chdir() e chroot().
chroot() pode ser usado nas versões CLI e CGI do PHP, mas requer que o programa seja executado com privilégios de root. chroot() na verdade altera o caminho do processo atual do diretório raiz para o diretório especificado. Isso permite que o processo atual execute apenas arquivos que existem nesse diretório. Freqüentemente, chroot() é usado pelos servidores como um “dispositivo de segurança” para garantir que códigos maliciosos não modifiquem arquivos fora de um diretório específico. Tenha em mente que embora chroot() impeça você de acessar qualquer arquivo fora do seu novo diretório, qualquer recurso de arquivo atualmente aberto ainda poderá ser acessado. Por exemplo, o código a seguir pode abrir um arquivo de log, chamar chroot() e alternar para um diretório de dados, ainda assim conseguir efetuar login e abrir o recurso do arquivo:
<?php;
$logfile = fopen("/var/log/chroot.log", "w");
chroot("/Usuários/george");
fputs($logfile, "Olá de dentro do Chrootn");
?>
Se uma aplicação não puder usar chroot(), então você pode chamar chdir() para definir o diretório de trabalho. Isso é útil, por exemplo, quando o código precisa carregar um código específico que pode estar localizado em qualquer lugar do sistema. Observe que chdir() não fornece nenhum mecanismo de segurança para impedir a abertura não autorizada de arquivos.
2. Desistir de privilégios
Ao escrever daemons Unix, uma precaução de segurança clássica é fazer com que eles desistam de todos os privilégios desnecessários; caso contrário, ter privilégios desnecessários pode facilmente levar a problemas desnecessários; No caso de vulnerabilidades no código (ou no próprio PHP), os danos muitas vezes podem ser minimizados garantindo que um daemon seja executado como um usuário menos privilegiado.
Uma maneira de fazer isso é executar o daemon como um usuário sem privilégios. No entanto, isso geralmente não é suficiente se o programa precisar abrir inicialmente recursos que usuários sem privilégios não têm permissão para abrir (como arquivos de log, arquivos de dados, soquetes, etc.).
Se você estiver executando como root, poderá abrir mão de seus privilégios com a ajuda das funções posix_setuid() e posiz_setgid(). O exemplo a seguir altera os privilégios do programa atualmente em execução para aqueles pertencentes ao usuário ninguém:
$pw=posix_getpwnam('nobody');
posix_setuid($pw['uid']);
posix_setgid($pw['gid']);
Assim como chroot(), quaisquer recursos privilegiados que foram abertos antes de abrir mão dos privilégios permanecerão abertos, mas novos recursos não poderão ser criados.
3. Garantir exclusividade
Muitas vezes você pode querer alcançar: apenas uma instância de um script está sendo executada por vez. Isso é particularmente importante para proteger scripts, pois executá-los em segundo plano pode facilmente levar à chamada acidental de várias instâncias.
A técnica padrão para garantir essa exclusividade é fazer com que o script bloqueie um arquivo específico (geralmente um arquivo bloqueado e usado exclusivamente) usando rebanho(). Se o bloqueio falhar, o script deverá imprimir um erro e sair. Aqui está um exemplo:
$fp=fopen("/tmp/.lockfile","a");
if(!$fp || !flock($fp, LOCK_EX | LOCK_NB)) {
fputs(STDERR, "Falha ao adquirir bloqueion");
saída;
}
/*Bloqueado com sucesso para realizar trabalho com segurança*/
Observe que a discussão do mecanismo de bloqueio envolve mais conteúdo e não será explicada aqui.
4. Construindo o Serviço de Monitoramento
Nesta seção, usaremos PHP para escrever um mecanismo básico de monitoramento. Como você não saberá antecipadamente como alterá-lo, deverá tornar sua implementação flexível e possível.
O criador de logs deve ser capaz de suportar inspeção de serviços arbitrários (por exemplo, serviços HTTP e FTP) e registrar eventos de qualquer forma (via e-mail, saída para um arquivo de log, etc.). É claro que você deseja que ele seja executado como um daemon, portanto, você deve solicitar que ele mostre seu estado atual completo;
Um serviço precisa implementar a seguinte classe abstrata:
abstract class ServiceCheck {
const FALHA = 0;
const SUCESSO = 1;
protegido $tempo limite = 30;
protegido $next_attempt;
protegido $status_atual = ServiceCheck::SUCCESS;
protegido $previous_status = ServiceCheck::SUCCESS;
frequência protegida $ = 30;
descrição $protegida;
protegido $failures_consecutivos = 0;
protegido $status_time;
protegido $failure_time;
protegido $loggers = array();
função pública abstrata __construct($params);
função pública __call($nome, $args)
{
if(isset($este->$nome)) {
return $este->$nome;
}
}
função pública set_next_attempt()
{
$this->next_attempt = tempo() + $this->frequência;
}
função abstrata pública run();
função pública post_run($status)
{
if($status !== $this->status_atual) {
$this->status_anterior = $this->status_atual;
}
if($status === self::FAILURE) {
if( $this->status_atual === self::FAILURE ) {
$this->consecutive_failures++;
}
outro {
$this->failure_time = time();
}
}
outro {
$this->consecutive_failures = 0;
}
$this->status_time = tempo();
$this->status_atual = $status;
$this->log_service_event();
}
função pública log_current_status()
{
foreach($this->loggers as $logger) {
$logger->log_current_status($this);
}
}
função privada log_service_event()
{
foreach($this->loggers as $logger) {
$logger->log_service_event($this);
}
}
função pública registrador_logger(ServiceLogger $logger)
{
$this->loggers[] = $logger;
}
}
O método sobrecarregado __call() acima fornece acesso somente leitura aos parâmetros de um objeto ServiceCheck:
· timeout - quanto tempo esta verificação pode ser suspensa antes que o mecanismo termine a verificação.
· next_attempt - A próxima vez que tentar se conectar ao servidor.
· current_status - O status atual do serviço: SUCCESS ou FAILURE.
· previous_status - o status antes do status atual.
· frequência - com que frequência verificar o serviço.
· descrição - descrição do serviço.
· consecutivo_failures - O número de falhas consecutivas na verificação de serviço desde o último sucesso.
· status_time - A última vez que o serviço foi verificado.
· failed_time - Se o status for FAILED, representa o horário em que ocorreu a falha.
Esta classe também implementa o padrão Observer, permitindo que objetos do tipo ServiceLogger se registrem e o chamem quando log_current_status() ou log_service_event() for chamado.
A função chave implementada aqui é run(), que é responsável por definir como a verificação deve ser realizada. Se a verificação for bem-sucedida, deverá retornar SUCCESS; caso contrário, deverá retornar FAILURE.
Quando a verificação de serviço definida em run() retorna, o método post_run() é chamado. É responsável por definir o estado do objeto e implementar o log.
Interface ServiceLogger: A especificação de uma classe de log precisa apenas implementar dois métodos: log_service_event() e log_current_status(), que são chamados quando uma verificação run() retorna e quando uma solicitação de status normal é implementada.
A interface é a seguinte:
interface ServiceLogger {
função pública log_service_event(ServiceCheck$service);
função pública log_current_status(ServiceCheck$service);
}
Finalmente, você precisa escrever o próprio mecanismo. A ideia é semelhante àquela usada ao escrever o programa simples da seção anterior: o servidor deve criar um novo processo para lidar com cada verificação e usar um manipulador SIGCHLD para detectar o valor de retorno quando a verificação for concluída. O número máximo que pode ser verificado simultaneamente deverá ser configurável, evitando assim o uso excessivo de recursos do sistema. Todos os serviços e logs serão definidos em um arquivo XML.
Aqui está a classe ServiceCheckRunner que define este mecanismo:
class ServiceCheckRunner {
privado $num_filhos;
private $serviços = array();
private $filhos = array();
função pública _ _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();
}
outro {
fputs(STDERR, "{$logger->class} não pode ser instanciado.n");
saída;
}
}
foreach($conf->serviços->serviço as $serviço) {
$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->serviços[] = $item;
}
outro {
fputs(STDERR, "{$service->class} não é instanciável.n");
saída;
}
}
}
função privada next_attempt_sort($a, $b){
if($a->next_attempt() == $b->next_attempt()) {
retornar 0;
}
retornar ($a->next_attempt() < $b->next_attempt())?
}
função privada próxima(){
usort($this->services, array($this, 'next_attempt_sort'));
return $this->serviços[0];
}
loop de função pública(){
declarar(carrapatos=1);
pcntl_signal(SIGCHLD, array($this, "sig_child"));
pcntl_signal(SIGUSR1, array($this, "sig_usr1"));
enquanto(1) {
$agora = hora();
if(contar($this->filhos)< $this->num_filhos) {
$serviço = $this->next();
if($agora< $service->next_attempt()) {
dormir(1);
continuar;
}
$serviço->set_next_attempt();
if($pid = pcntl_fork()) {
$this->filhos[$pid] = $serviço;
}
outro {
pcntl_alarm($serviço->tempo limite());
exit($serviço->executar());
}
}
}
}
função pública log_current_status(){
foreach($this->serviços como $serviço) {
$serviço->log_current_status();
}
}
função privada sig_child($signal){
$status = ServiceCheck::FALHA;
pcntl_signal(SIGCHLD, array($this, "sig_child"));
while(($pid = pcntl_wait($status, WNOHANG)) > 0){
$serviço = $this->filhos[$pid];
unset($this->filhos[$pid]);
if(pcntl_wifexited($status) && pcntl_wexitstatus($status) ==ServiceCheck::SUCESSO)
{
$status = ServiceCheck::SUCESSO;
}
$serviço->post_run($status);
}
}
função privada sig_usr1($signal){
pcntl_signal(SIGUSR1, array($this, "sig_usr1"));
$this->log_current_status();
}
}
Esta é uma classe muito complexa. Seu construtor lê e analisa um arquivo XML, cria todos os serviços a serem monitorados e cria um registrador para registrá-los.
O método loop() é o método principal desta classe. Ele define o manipulador de sinal da solicitação e verifica se um novo processo filho pode ser criado. Agora, se o próximo evento (ordenado pelo tempo next_attempt CHUO) funcionar bem, um novo processo será criado. Dentro deste novo processo filho, emita um aviso para evitar que a duração do teste exceda seu limite de tempo e, em seguida, execute o teste definido por run().
Existem também dois manipuladores de sinal: o manipulador SIGCHLD sig_child(), que é responsável por coletar processos filhos encerrados e executar o método post_run() de seu serviço; o manipulador SIGUSR1 sig_usr1(), que simplesmente chama o método log_current_status() de todos os registradores registrados, que pode ser usado para obter o status atual de todo o sistema.
É claro que esta arquitetura de vigilância não faz nada de prático. Mas primeiro você precisa verificar um serviço. A classe a seguir verifica se você está recebendo uma resposta "200 Server OK" de um servidor HTTP:
class HTTP_ServiceCheck extends ServiceCheck{
url pública $;
função pública _ _construct($params){
foreach($params as $k => $v) {
$k = "$k";
$isto->$k = "$v";
}
}
função pública executar(){
if(is_resource(@fopen($this->url, "r"))) {
retornar ServiceCheck::SUCESSO;
}
outro {
retornar ServiceCheck::FALHA;
}
}
}
Comparado com os frameworks que você construiu antes, este serviço é extremamente simples e não será descrito em detalhes aqui.
5. Exemplo de processo do ServiceLogger
A seguir está um exemplo de processo do ServiceLogger. Quando um serviço está inativo, ele é responsável por enviar um e-mail para uma pessoa de plantão:
class EmailMe_ServiceLogger implements ServiceLogger {
função pública log_service_event(ServiceCheck$service)
{
if($serviço->status_atual ==ServiceCheck::FAILURE) {
$message = "Problema com{$serviço->descrição()}rn";
mail( '[email protected]' , 'Evento de Serviço', $mensagem);
if($serviço->consecutive_failures()> 5) {
mail( '[email protected]' , 'Evento de Serviço', $mensagem);
}
}
}
função pública log_current_status(ServiceCheck$service){
retornar;
}
}
Se falhar cinco vezes seguidas, o processo também envia uma mensagem para um endereço de backup. Observe que ele não implementa um método log_current_status() significativo.
Sempre que você alterar o estado de um serviço da seguinte forma, você deve implementar um processo ServiceLogger que grava no log de erros do PHP:
class ErrorLog_ServiceLogger implements ServiceLogger {
função pública log_service_event(ServiceCheck$service)
{
if($serviço->status_atual() !==$serviço->status_anterior()) {
if($service->current_status() ===ServiceCheck::FAILURE) {
$status = 'PARA BAIXO';
}
outro {
$status = 'PARA CIMA';
}
error_log("{$service->description()} alterou o status para $status");
}
}
função pública log_current_status(ServiceCheck$service)
{
error_log("{$serviço->descrição()}: $status");
}
}
O método log_current_status() significa que se um processo enviar um sinal SIGUSR1, ele copiará seu status atual completo para seu log de erros do PHP.
O mecanismo usa um arquivo de configuração como segue:
<config>
<loggers>
<logger>
<id>registo de erros</id>
<class>ErrorLog_ServiceLogger</class>
</logger>
<logger>
<id>e-mailme</id>
<class>EmailMe_ServiceLogger</class>
</logger>
</loggers>
<serviços>
<serviço>
<class>HTTP_ServiceCheck</class>
<parâmetros>
<descrição>Verificação HTTP OmniTI</descrição>
<url> http://www.omniti.com </url>
<tempo limite>30</tempo limite>
<frequência>900</frequência>
</parâmetros>
<loggers>
<logger>errorlog</logger>
<logger>emailme</logger>
</loggers>
</serviço>
<serviço>
<class>HTTP_ServiceCheck</class>
<parâmetros>
<descrição>Verificação HTTP da página inicial</descrição>
<url> http://www.schlossnagle.org/~george </url>
<tempo limite>30</tempo limite>
<frequência>3600</frequência>
</parâmetros>
<loggers>
<logger>errorlog</logger>
</loggers>
</serviço>
</serviços>
</config>
Quando esse arquivo XML é passado, o construtor de ServiceCheckRunner instancia um programa de registro para cada log especificado. Em seguida, ele instancia um objeto ServiceCheck correspondente a cada serviço especificado.
Observe que o construtor usa a classe Reflection_Class para implementar verificações internas do serviço e das classes de registro - antes de tentar instanciá-las. Embora isso seja desnecessário, demonstra bem o uso da nova API Reflection no PHP 5. Além dessas classes, a API Reflection fornece classes para implementar a inspeção intrínseca de quase qualquer entidade interna (classe, método ou função) em PHP.
Para usar o mecanismo que você construiu, você ainda precisa de algum código wrapper. O watchdog deve impedir que você tente iniciá-lo duas vezes - você não precisa criar duas mensagens para cada evento. Claro, o monitor também deve receber algumas opções, incluindo:
Descrição da opção
[-f] Um local para o arquivo de configuração do mecanismo. O padrão é monitor.xml.
[-n] O tamanho do pool de processos filho permitido pelo mecanismo. O padrão é 5.
[-d] Um sinalizador que desativa a funcionalidade do daemon deste mecanismo. Isso é útil quando você escreve um processo de depuração do ServiceLogger que gera informações para stdout ou stderr.
Aqui está o script watchdog final que analisa opções, garante exclusividade e executa verificações de serviço:
require_once "Service.inc";
require_once "Console/Gopt.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, "Falha ao adquirir bloqueion");
saída;
}
if(!$args['d']) {
if(pcntl_fork()) {
saída;
}
posix_setsid();
if(pcntl_fork()) {
saída;
}
}
fwrite($fp, getmypid());
flush($fp);
$motor = novo ServiceCheckRunner($args['f'], $args['n']);
$engine->loop();
Observe que este exemplo usa a função getOptions() personalizada.
Depois de escrever um arquivo de configuração apropriado, você pode iniciar o script da seguinte forma:
> ./monitor.php -f /etc/monitor.xml
Isso protege e continua o monitoramento até que a máquina seja desligada ou o script seja eliminado.
Este script é bastante complexo, mas ainda existem algumas áreas facilmente melhoráveis, que ficam como exercício para o leitor:
· Adicione um manipulador SIGHUP que reanalisa o arquivo de configuração para que você possa alterar a configuração sem iniciar o servidor.
· Escreva um ServiceLogger que possa fazer login em um banco de dados para armazenar dados de consulta.
· Escreva um programa front-end web para fornecer uma boa GUI para todo o sistema de monitoramento.