Resumen: En este artículo, analizaremos las numerosas técnicas y precauciones para crear un motor de monitoreo del lado del servidor básico basado en el lenguaje PHP y brindaremos una implementación completa del código fuente.
1. El problema de cambiar el directorio de trabajo
Cuando escribe un programa de monitoreo, generalmente es mejor dejar que establezca su propio directorio de trabajo. De esta manera, si utiliza una ruta relativa para leer y escribir archivos, automáticamente manejará la ubicación donde el usuario espera que se almacene el archivo según la situación. Aunque es una buena práctica limitar siempre las rutas utilizadas en un programa, pierde la flexibilidad que merece; Por lo tanto, la forma más segura de cambiar su directorio de trabajo es usar chdir() y chroot().
chroot() se puede utilizar en las versiones CLI y CGI de PHP, pero requiere que el programa se ejecute con privilegios de root. chroot() en realidad cambia la ruta del proceso actual desde el directorio raíz al directorio especificado. Esto permite que el proceso actual ejecute solo archivos que existen en ese directorio. A menudo, los servidores utilizan chroot() como un "dispositivo de seguridad" para garantizar que el código malicioso no modifique archivos fuera de un directorio específico. Tenga en cuenta que aunque chroot() le impide acceder a cualquier archivo fuera de su nuevo directorio, aún se puede acceder a cualquier recurso de archivo abierto actualmente. Por ejemplo, el siguiente código puede abrir un archivo de registro, llamar a chroot() y cambiar a un directorio de datos, luego aún podrá iniciar sesión correctamente y abrir el recurso del archivo:
<?php;
$archivo de registro = fopen("/var/log/chroot.log", "w");
chroot("/Usuarios/george");
fputs($logfile, "Hola desde dentro del Chrootn");
?>
Si una aplicación no puede usar chroot(), entonces puede llamar a chdir() para configurar el directorio de trabajo. Esto es útil, por ejemplo, cuando el código necesita cargar un código específico que puede ubicarse en cualquier parte del sistema. Tenga en cuenta que chdir() no proporciona ningún mecanismo de seguridad para evitar la apertura no autorizada de archivos.
2. Renunciar a privilegios
Al escribir demonios Unix, una precaución de seguridad clásica es hacer que renuncien a todos los privilegios innecesarios, de lo contrario, tener privilegios innecesarios puede conducir fácilmente a problemas innecesarios. En el caso de vulnerabilidades en el código (o en el propio PHP), el daño a menudo se puede minimizar asegurando que un demonio se ejecute como un usuario con menos privilegios.
Una forma de lograr esto es ejecutar el demonio como un usuario sin privilegios. Sin embargo, esto generalmente no es suficiente si el programa necesita abrir inicialmente recursos para los que los usuarios sin privilegios no tienen permiso (como archivos de registro, archivos de datos, sockets, etc.).
Si está ejecutando como root, puede renunciar a sus privilegios con la ayuda de las funciones posix_setuid() y posiz_setgid(). El siguiente ejemplo cambia los privilegios del programa que se está ejecutando actualmente a aquellos que pertenecen al usuario nadie:
$pw=posix_getpwnam('nobody');
posix_setuid($pw['uid']);
posix_setgid($pw['gid']);
Al igual que chroot(), todos los recursos privilegiados que se abrieron antes de renunciar a los privilegios permanecerán abiertos, pero no se podrán crear nuevos recursos.
3. Garantizar la exclusividad
Es posible que a menudo desees lograr que solo se ejecute una instancia de un script en cualquier momento. Esto es particularmente importante para proteger los scripts, ya que ejecutarlos en segundo plano puede provocar fácilmente que se llamen accidentalmente a varias instancias.
La técnica estándar para garantizar esta exclusividad es hacer que el script bloquee un archivo específico (a menudo un archivo bloqueado y de uso exclusivo) mediante el uso de grey(). Si el bloqueo falla, el script debería imprimir un error y salir. Aquí hay un ejemplo:
$fp=fopen("/tmp/.lockfile","a");
if(!$fp || !rebaño($fp, LOCK_EX | LOCK_NB)) {
fputs(STDERR, "Error al adquirir el bloqueon");
salida;
}
/*Bloqueado con éxito para realizar el trabajo de forma segura*/
Tenga en cuenta que la discusión sobre el mecanismo de bloqueo implica más contenido y no se explicará aquí.
4. Construyendo el servicio de monitoreo
En esta sección, usaremos PHP para escribir un motor de monitoreo básico. Como no sabrá de antemano cómo cambiarlo, debe hacer que su implementación sea flexible y posible.
El registrador debe poder admitir la inspección de servicios arbitrarios (por ejemplo, servicios HTTP y FTP) y poder registrar eventos de cualquier forma (por correo electrónico, salida a un archivo de registro, etc.). Por supuesto, desea que se ejecute como un demonio, por lo tanto, debe pedirle que muestre su estado actual completo;
Un servicio necesita implementar la siguiente clase abstracta:
clase abstracta ServiceCheck {
constante FALLO = 0;
ÉXITO constante = 1;
protegido $tiempo de espera = 30;
protegido $siguiente_intento;
protegido $current_status = ServiceCheck::SUCCESS;
protegido $estado_anterior = ServiceCheck::SUCCESS;
frecuencia $protegida = 30;
descripción $ protegida;
protegido $consecutive_failures = 0;
protegido $status_time;
protegido $tiempo_fallo;
protegido $loggers = matriz();
función pública abstracta __construct($params);
función pública __call($nombre, $args)
{
if(isset($this->$nombre)) {
devolver $this->$nombre;
}
}
función pública set_next_attempt()
{
$this->next_attempt = tiempo() + $this->frecuencia;
}
ejecución de función abstracta pública();
función pública post_run($estado)
{
if($estado!== $this->estado_actual) {
$this->estado_anterior = $this->estado_actual;
}
if($estado === self::FALLO) {
if( $this->current_status === self::FAILURE ) {
$this->consecutive_failures++;
}
demás {
$this->failure_time = tiempo();
}
}
demás {
$this->consecutive_failures = 0;
}
$this->status_time = tiempo();
$this->current_status = $estado;
$this->log_service_event();
}
función pública log_current_status()
{
foreach($this->loggers como $logger) {
$logger->log_current_status($this);
}
}
función privada log_service_event()
{
foreach($this->loggers como $logger) {
$logger->log_service_event($this);
}
}
función pública registrar_logger(ServiceLogger $logger)
{
$this->loggers[] = $logger;
}
}
El método sobrecargado __call() anterior proporciona acceso de solo lectura a los parámetros de un objeto ServiceCheck:
· tiempo de espera: cuánto tiempo se puede suspender esta verificación antes de que el motor finalice la verificación.
· next_attempt: la próxima vez que intentaremos conectarnos al servidor.
· current_status: el estado actual del servicio: ÉXITO o FALLO.
· estado_anterior: el estado anterior al estado actual.
· frecuencia: con qué frecuencia comprobar el servicio.
· descripción - descripción del servicio.
· consecutivos_failures: el número de errores consecutivos de verificación del servicio desde el último éxito.
· status_time: la última vez que se verificó el servicio.
· fail_time: si el estado es FALLADO, representa el momento en que ocurrió el error.
Esta clase también implementa el patrón Observer, permitiendo que los objetos de tipo ServiceLogger se registren y luego lo llamen cuando se llama a log_current_status() o log_service_event().
La función clave implementada aquí es run(), que es responsable de definir cómo se debe realizar la verificación. Si la verificación tiene éxito, debería devolver ÉXITO; de lo contrario, debería devolver FALLO.
Cuando regresa la verificación del servicio definida en run(), se llama al método post_run(). Es responsable de establecer el estado del objeto e implementar el registro.
Interfaz ServiceLogger: para especificar una clase de registro solo es necesario implementar dos métodos: log_service_event() y log_current_status(), que se llaman cuando regresa una verificación run() y cuando se implementa una solicitud de estado normal.
La interfaz es la siguiente:
interfaz ServiceLogger {
función pública log_service_event(ServiceCheck$servicio);
función pública log_current_status(ServiceCheck$servicio);
}
Finalmente, necesitas escribir el motor en sí. La idea es similar a la utilizada al escribir el programa simple en la sección anterior: el servidor debe crear un nuevo proceso para manejar cada verificación y usar un controlador SIGCHLD para detectar el valor de retorno cuando se completa la verificación. El número máximo que se puede comprobar simultáneamente debe ser configurable, evitando así el uso excesivo de recursos del sistema. Todos los servicios y registros se definirán en un archivo XML.
Aquí está la clase ServiceCheckRunner que define este motor:
clase ServiceCheckRunner {
privado $num_niños;
privado $servicios = matriz();
privado $niños = matriz();
función pública _ _construct($conf, $num_children)
{
$loggers = matriz();
$this->núm_niños = $núm_niños;
$conf = simplexml_load_file($conf);
foreach($conf->loggers->logger como $logger) {
$clase = new Reflection_Class("$logger->clase");
if($clase->isInstantiable()) {
$loggers["$logger->id"] = $clase->newInstance();
}
demás {
fputs(STDERR, "No se puede crear una instancia de {$logger->class}.n");
salida;
}
}
foreach($conf->servicios->servicio como $servicio) {
$clase = new Reflection_Class("$servicio->clase");
if($clase->isInstantiable()) {
$elemento = $clase->newInstance($servicio->params);
foreach($servicio->loggers->logger como $logger) {
$item->register_logger($loggers["$logger"]);
}
$this->servicios[] = $artículo;
}
demás {
fputs(STDERR, "{$servicio->clase} no es instanciable.n");
salida;
}
}
}
función privada next_attempt_sort($a, $b){
if($a->siguiente_intento() == $b->siguiente_intento()) {
devolver 0;
}
return ($a->siguiente_intento() < $b->siguiente_intento()? -1: 1;
}
función privada siguiente(){
usort($this->services, array($this, 'next_attempt_sort'));
devolver $this->servicios[0];
}
bucle de función pública(){
declarar(ticks=1);
pcntl_signal(SIGCHLD, array($this, "sig_child"));
pcntl_signal(SIGUSR1, array($this, "sig_usr1"));
mientras(1) {
$ahora = tiempo();
if(count($this->niños)< $this->num_children) {
$servicio = $this->siguiente();
if($ahora < $servicio->next_attempt()) {
dormir(1);
continuar;
}
$servicio->set_next_attempt();
si($pid = pcntl_fork()) {
$this->children[$pid] = $servicio;
}
demás {
pcntl_alarm($servicio->timeout());
salir($servicio->ejecutar());
}
}
}
}
función pública log_current_status(){
foreach($this->servicios como $servicio) {
$servicio->log_current_status();
}
}
función privada sig_child($señal){
$estado = ServiceCheck::FALLO;
pcntl_signal(SIGCHLD, array($this, "sig_child"));
while(($pid = pcntl_wait($estado, WNOHANG)) > 0){
$servicio = $this->children[$pid];
unset($this->niños[$pid]);
if(pcntl_wifexited($status) && pcntl_wexitstatus($status) ==ServiceCheck::ÉXITO)
{
$estado = ServiceCheck::ÉXITO;
}
$servicio->post_run($estado);
}
}
función privada sig_usr1($señal){
pcntl_signal(SIGUSR1, array($this, "sig_usr1"));
$this->log_current_status();
}
}
Esta es una clase muy compleja. Su constructor lee y analiza un archivo XML, crea todos los servicios que se van a monitorear y crea un registrador para registrarlos.
El método loop() es el método principal de esta clase. Establece el controlador de señales de la solicitud y verifica si se puede crear un nuevo proceso hijo. Ahora, si el siguiente evento (ordenado por next_attempt time CHUO) se ejecuta bien, se creará un nuevo proceso. Dentro de este nuevo proceso hijo, emita una advertencia para evitar que la duración de la prueba exceda su límite de tiempo y luego ejecute la prueba definida por run().
También hay dos manejadores de señales: el manejador SIGCHLD sig_child(), que es responsable de recopilar procesos secundarios terminados y ejecutar el método post_run() de su servicio; el manejador SIGUSR1 sig_usr1(), que simplemente llama al método log_current_status() de todos los registradores registrados, que se puede utilizar para obtener el estado actual de todo el sistema.
Por supuesto, esta arquitectura de vigilancia no hace nada práctico. Pero primero, debes consultar un servicio. La siguiente clase comprueba si obtiene una respuesta "200 Server OK" de un servidor HTTP:
clase HTTP_ServiceCheck extiende ServiceCheck{
URL pública $;
función pública _ _construct($params){
foreach($params como $k => $v) {
$k = "$k";
$esto->$k = "$v";
}
}
ejecución de función pública(){
si(is_resource(@fopen($this->url, "r"))) {
devolver ServiceCheck::ÉXITO;
}
demás {
devolver ServiceCheck::FALLO;
}
}
}
En comparación con los marcos que ha creado antes, este servicio es extremadamente simple y no se describirá en detalle aquí.
5. Ejemplo de proceso ServiceLogger
El siguiente es un ejemplo de proceso ServiceLogger. Cuando un servicio no funciona, es responsable de enviar un correo electrónico a una persona de guardia:
clase EmailMe_ServiceLogger implementa ServiceLogger {
función pública log_service_event(ServiceCheck$servicio)
{
if($servicio->estado_actual ==ServiceCheck::FAILURE) {
$mensaje = "Problema con{$servicio->descripción()}rn";
mail( '[email protected]' , 'Evento de servicio', $mensaje);
if($servicio->fallos_consecutivos()> 5) {
mail( '[email protected]' , 'Evento de servicio', $mensaje);
}
}
}
función pública log_current_status(ServiceCheck$servicio){
devolver;
}
}
Si falla cinco veces seguidas, el proceso también envía un mensaje a una dirección de respaldo. Tenga en cuenta que no implementa un método log_current_status() significativo.
Siempre que cambie el estado de un servicio de la siguiente manera, debe implementar un proceso ServiceLogger que escriba en el registro de errores de PHP:
class ErrorLog_ServiceLogger implements ServiceLogger {
función pública log_service_event(ServiceCheck$servicio)
{
if($servicio->estado_actual()!==$servicio->estado_anterior()) {
if($servicio->estado_actual() ===ServiceCheck::FAILURE) {
$estado = 'ABAJO';
}
demás {
$estado = 'ARRIBA';
}
error_log("{$service->description()} cambió el estado a $status");
}
}
función pública log_current_status(ServiceCheck$servicio)
{
error_log("{$servicio->descripción()}: $estado");
}
}
El método log_current_status() significa que si un proceso envía una señal SIGUSR1, copiará su estado actual completo en su registro de errores de PHP.
El motor utiliza un archivo de configuración de la siguiente manera:
<config>
<madereros>
<registrador>
<id>registro de errores</id>
<clase>ErrorLog_ServiceLogger</clase>
</registrador>
<registrador>
<id>envíeme un correo electrónico</id>
<clase>EmailMe_ServiceLogger</clase>
</registrador>
</Registradores>
<servicios>
<servicio>
<clase>HTTP_ServiceCheck</clase>
<parámetros>
<descripción>OmniTI HTTP Check</descripción>
<url> http://www.omniti.com </url>
<tiempo de espera>30</tiempo de espera>
<frecuencia>900</frecuencia>
</params>
<madereros>
<logger>errorlog</logger>
<logger>envíeme un correo electrónico</logger>
</Registradores>
</servicio>
<servicio>
<clase>HTTP_ServiceCheck</clase>
<parámetros>
<descripción>Comprobación HTTP de la página de inicio</descripción>
<url> http://www.schlossnagle.org/~george </url>
<tiempo de espera>30</tiempo de espera>
<frecuencia>3600</frecuencia>
</params>
<madereros>
<logger>errorlog</logger>
</Registradores>
</servicio>
</servicios>
</config>
Cuando se pasa este archivo XML, el constructor de ServiceCheckRunner crea una instancia de un programa de registro para cada registro especificado. Luego, crea una instancia de un objeto ServiceCheck correspondiente a cada servicio especificado.
Tenga en cuenta que el constructor utiliza la clase Reflection_Class para implementar comprobaciones internas del servicio y las clases de registro, antes de intentar crear una instancia de ellas. Aunque esto es innecesario, demuestra muy bien el uso de la nueva API Reflection en PHP 5. Además de estas clases, la API de Reflection proporciona clases para implementar la inspección intrínseca de casi cualquier entidad interna (clase, método o función) en PHP.
Para utilizar el motor que creó, aún necesita algún código contenedor. El mecanismo de vigilancia debería impedirle intentar iniciarlo dos veces; no es necesario crear dos mensajes para cada evento. Por supuesto, el monitor también debería recibir algunas opciones que incluyen:
Descripción de la opción
[-f] Una ubicación para el archivo de configuración del motor. El valor predeterminado es monitor.xml.
[-n] El tamaño del grupo de procesos secundarios permitido por el motor. El valor predeterminado es 5.
[-d] Un indicador que deshabilita la funcionalidad del demonio de este motor. Esto es útil cuando escribe un proceso de depuración de ServiceLogger que genera información en stdout o stderr.
Aquí está el script de vigilancia final que analiza las opciones, garantiza la exclusividad y ejecuta comprobaciones de servicio:
require_once "Service.inc";
require_once "Consola/Getopt.php";
$opcionescortas = "n:f:d";
$default_opts = array('n' => 5, 'f' =>'monitor.xml');
$args = getOptions($default_opts, $shortoptions, nulo);
$fp = fopen("/tmp/.lockfile", "a");
if(!$fp || !rebaño($fp, LOCK_EX | LOCK_NB)) {
fputs($stderr, "Error al adquirir el bloqueon");
salida;
}
si(!$args['d']) {
si(pcntl_fork()) {
salida;
}
posix_setsid();
si(pcntl_fork()) {
salida;
}
}
fwrite($fp, getmypid());
Flush($fp);
$motor = nuevo ServiceCheckRunner($args['f'], $args['n']);
$engine->loop();
Tenga en cuenta que este ejemplo utiliza la función getOptions() personalizada.
Después de escribir un archivo de configuración apropiado, puede iniciar el script de la siguiente manera:
> ./monitor.php -f /etc/monitor.xml
Esto protege y continúa monitoreando hasta que se apaga la máquina o se elimina el script.
Este script es bastante complejo, pero todavía hay algunas áreas que se pueden mejorar fácilmente, que se dejan como ejercicio para el lector:
· Agregue un controlador SIGHUP que vuelva a analizar el archivo de configuración para que pueda cambiar la configuración sin iniciar el servidor.
· Escribe un ServiceLogger que pueda iniciar sesión en una base de datos para almacenar datos de consulta.
· Escribir un programa web front-end para proporcionar una buena GUI para todo el sistema de monitoreo.