Résumé : Dans cet article, discutons des nombreuses techniques et précautions pour créer un moteur de surveillance de base côté serveur basé sur le langage PHP, et donnons une implémentation complète du code source.
1. Le problème du changement de répertoire de travail
Lorsque vous écrivez un programme de surveillance, il est généralement préférable de le laisser définir son propre répertoire de travail. De cette façon, si vous utilisez un chemin relatif pour lire et écrire des fichiers, il gérera automatiquement l'emplacement où l'utilisateur s'attend à ce que le fichier soit stocké en fonction de la situation. Bien que ce soit une bonne pratique de toujours limiter les chemins utilisés dans un programme, cela perd la flexibilité qu'il mérite ; Par conséquent, le moyen le plus sûr de modifier votre répertoire de travail est d'utiliser à la fois chdir() et chroot().
chroot() peut être utilisé dans les versions CLI et CGI de PHP, mais il nécessite que le programme soit exécuté avec les privilèges root. chroot() modifie en fait le chemin du processus en cours du répertoire racine vers le répertoire spécifié. Cela permet au processus actuel d'exécuter uniquement les fichiers qui existent dans ce répertoire. Souvent, chroot() est utilisé par les serveurs comme « dispositif de sécurité » pour garantir qu'un code malveillant ne modifie pas les fichiers en dehors d'un répertoire spécifique. Gardez à l'esprit que même si chroot() vous empêche d'accéder à des fichiers en dehors de votre nouveau répertoire, toutes les ressources de fichiers actuellement ouvertes sont toujours accessibles. Par exemple, le code suivant peut ouvrir un fichier journal, appeler chroot() et basculer vers un répertoire de données, puis toujours pouvoir se connecter avec succès et ouvrir la ressource de fichier :
<?php
$logfile = fopen("/var/log/chroot.log", "w");
chroot("/Utilisateurs/george");
fputs($logfile, "Bonjour de l'intérieur du chrootn");
?>
Si une application ne peut pas utiliser chroot(), alors vous pouvez appeler chdir() pour définir le répertoire de travail. Ceci est utile, par exemple, lorsque le code doit charger un code spécifique pouvant se trouver n'importe où dans le système. Notez que chdir() ne fournit aucun mécanisme de sécurité pour empêcher l'ouverture non autorisée de fichiers.
2. Abandonner les privilèges
Lors de l'écriture de démons Unix, une précaution de sécurité classique consiste à leur faire renoncer à tous les privilèges inutiles ; sinon, avoir des privilèges inutiles peut facilement entraîner des problèmes inutiles. Dans le cas de vulnérabilités dans le code (ou dans PHP lui-même), les dégâts peuvent souvent être minimisés en garantissant qu'un démon s'exécute en tant qu'utilisateur le moins privilégié.
Une façon d'y parvenir consiste à exécuter le démon en tant qu'utilisateur non privilégié. Cependant, cela n'est généralement pas suffisant si le programme doit initialement ouvrir des ressources que les utilisateurs non privilégiés ne sont pas autorisés à ouvrir (telles que des fichiers journaux, des fichiers de données, des sockets, etc.).
Si vous exécutez en tant que root, vous pouvez renoncer à vos privilèges à l'aide des fonctions posix_setuid() et posiz_setgid(). L'exemple suivant remplace les privilèges du programme en cours d'exécution par ceux appartenant à l'utilisateur personne :
$pw=posix_getpwnam('nobody');
posix_setuid($pw['uid']);
posix_setgid($pw['gid']);
Tout comme chroot(), toutes les ressources privilégiées qui ont été ouvertes avant d'abandonner les privilèges resteront ouvertes, mais de nouvelles ressources ne pourront pas être créées.
3. Garantir l'exclusivité
Vous souhaitez souvent obtenir ce résultat : une seule instance d'un script est exécutée à la fois. Ceci est particulièrement important pour protéger les scripts, car leur exécution en arrière-plan peut facilement conduire à l'appel accidentel de plusieurs instances.
La technique standard pour garantir cette exclusivité consiste à demander au script de verrouiller un fichier spécifique (souvent un fichier verrouillé et utilisé exclusivement) à l'aide de flock(). Si le verrou échoue, le script doit imprimer une erreur et se terminer. Voici un exemple :
$fp=fopen("/tmp/.lockfile","a");
si(!$fp || !flock($fp, LOCK_EX | LOCK_NB)) {
fputs(STDERR, "Échec de l'acquisition du verroun");
sortie;
}
/*Verrouillé avec succès pour effectuer un travail en toute sécurité*/
Notez que la discussion sur le mécanisme de verrouillage implique plus de contenu et ne sera pas expliquée ici.
4. Construire le service de surveillance
Dans cette section, nous utiliserons PHP pour écrire un moteur de surveillance de base. Puisque vous ne saurez pas à l’avance comment le modifier, vous devez rendre sa mise en œuvre flexible et possible.
L'enregistreur doit être capable de prendre en charge l'inspection de services arbitraires (par exemple, les services HTTP et FTP) et de consigner les événements de quelque manière que ce soit (par e-mail, sortie dans un fichier journal, etc.). Bien sûr, vous souhaitez qu'il s'exécute en tant que démon ; vous devez donc lui demander d'afficher son état actuel complet.
Un service doit implémenter la classe abstraite suivante :
abstract class ServiceCheck {
const ÉCHEC = 0 ;
const SUCCÈS = 1 ;
protégé $timeout = 30 ;
protégé $next_attempt ;
protégé $current_status = ServiceCheck :: SUCCESS ;
protégé $previous_status = ServiceCheck :: SUCCESS ;
fréquence $ protégée = 30 ;
description $ protégée ;
protégé $consecutive_failures = 0 ;
protégé $status_time ;
protégé $failure_time ;
protégé $loggers = array();
fonction publique abstraite __construct($params);
fonction publique __call($name, $args)
{
if(isset($this->$name)) {
retourner $this->$nom ;
}
}
fonction publique set_next_attempt()
{
$this->next_attempt = time() + $this->fréquence ;
}
fonction abstraite publique run();
fonction publique post_run($status)
{
if($status !== $this->current_status) {
$this->previous_status = $this->current_status ;
}
si($statut === self::FAILURE) {
if( $this->current_status === self::FAILURE ) {
$this->consecutive_failures++;
}
autre {
$this->failure_time = time();
}
}
autre {
$this->consecutive_failures = 0;
}
$this->status_time = time();
$this->current_status = $statut ;
$this->log_service_event();
}
fonction publique log_current_status()
{
foreach($this->loggers as $logger) {
$logger->log_current_status($this);
}
}
fonction privée log_service_event()
{
foreach($this->loggers as $logger) {
$logger->log_service_event($this);
}
}
fonction publique register_logger (ServiceLogger $ logger)
{
$this->loggers[] = $logger;
}
}
La méthode surchargée __call() ci-dessus fournit un accès en lecture seule aux paramètres d'un objet ServiceCheck :
· timeout - combien de temps cette vérification peut être suspendue avant que le moteur n'y mette fin.
· next_attempt - La prochaine fois pour tenter de se connecter au serveur.
· current_status - L'état actuel du service : SUCCÈS ou ÉCHEC.
· previous_status - le statut avant le statut actuel.
· fréquence - à quelle fréquence vérifier le service.
· description - description du service.
· consécutif_failures - Nombre d'échecs consécutifs de contrôle de service depuis le dernier succès.
· status_time - La dernière fois que le service a été vérifié.
· Failure_time - Si l'état est FAILED, il représente l'heure à laquelle l'échec s'est produit.
Cette classe implémente également le modèle Observer, permettant aux objets de type ServiceLogger de s'enregistrer puis de l'appeler lorsque log_current_status() ou log_service_event() est appelé.
La fonction clé implémentée ici est run(), qui est chargée de définir la manière dont la vérification doit être effectuée. Si la vérification réussit, elle doit renvoyer SUCCESS ; sinon, elle doit renvoyer FAILURE.
Lorsque le contrôle de service défini dans run() est renvoyé, la méthode post_run() est appelée. Il est responsable de la définition de l’état de l’objet et de la mise en œuvre de la journalisation.
Interface ServiceLogger : la spécification d'une classe de journal ne nécessite que l'implémentation de deux méthodes : log_service_event() et log_current_status(), qui sont appelées lorsqu'une vérification run() revient et lorsqu'une demande d'état normale est implémentée.
L'interface est la suivante :
interface ServiceLogger {
fonction publique log_service_event(ServiceCheck$service);
fonction publique log_current_status(ServiceCheck$service);
}
Enfin, vous devez écrire le moteur lui-même. L'idée est similaire à celle utilisée lors de l'écriture du programme simple dans la section précédente : le serveur doit créer un nouveau processus pour gérer chaque vérification et utiliser un gestionnaire SIGCHLD pour détecter la valeur de retour lorsque la vérification est terminée. Le nombre maximum pouvant être vérifié simultanément doit être configurable, évitant ainsi une utilisation excessive des ressources système. Tous les services et journaux seront définis dans un fichier XML.
Voici la classe ServiceCheckRunner qui définit ce moteur :
class ServiceCheckRunner {
privé $num_children ;
privé $services = tableau();
privé $enfants = tableau();
fonction publique _ _construct($conf, $num_children)
{
$enregistreurs = tableau();
$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();
}
autre {
fputs(STDERR, "{$logger->class} ne peut pas être instancié.n");
sortie;
}
}
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;
}
autre {
fputs(STDERR, "{$service->class} n'est pas instanciable.n");
sortie;
}
}
}
fonction privée next_attempt_sort($a, $b){
if($a->next_attempt() == $b->next_attempt()) {
renvoie 0 ;
}
return ($a->next_attempt() < $b->next_attempt()) ?
}
fonction privée suivant(){
usort($this->services, array($this, 'next_attempt_sort'));
retourner $this->services[0] ;
}
boucle de fonction publique(){
déclarer(tiques=1);
pcntl_signal(SIGCHLD, array($this, "sig_child"));
pcntl_signal(SIGUSR1, array($this, "sig_usr1"));
tandis que(1) {
$maintenant = heure();
if(count($this->children)< $this->num_children) {
$service = $this->next();
if($maintenant < $service->next_attempt()) {
dormir(1);
continuer;
}
$service->set_next_attempt();
si($pid = pcntl_fork()) {
$this->enfants[$pid] = $service;
}
autre {
pcntl_alarm($service->timeout());
exit($service->run());
}
}
}
}
fonction publique log_current_status(){
foreach($this->services en tant que $service) {
$service->log_current_status();
}
}
fonction privée sig_child ($ signal) {
$statut = ServiceCheck :: ÉCHEC ;
pcntl_signal(SIGCHLD, array($this, "sig_child"));
while(($pid = pcntl_wait($status, WNOHANG)) > 0){
$service = $this->enfants[$pid];
unset($this->enfants[$pid]);
if(pcntl_wifexited($status) && pcntl_wexitstatus($status) ==ServiceCheck::SUCCESS)
{
$statut = ServiceCheck :: SUCCÈS ;
}
$service->post_run($status);
}
}
fonction privée sig_usr1($signal){
pcntl_signal(SIGUSR1, array($this, "sig_usr1"));
$this->log_current_status();
}
}
Il s'agit d'une classe très complexe. Son constructeur lit et analyse un fichier XML, crée tous les services à surveiller et crée un enregistreur pour les enregistrer.
La méthode loop() est la méthode principale de cette classe. Il définit le gestionnaire de signal de la requête et vérifie si un nouveau processus enfant peut être créé. Désormais, si le prochain événement (ordonné par next_attempt time CHUO) se déroule bien, un nouveau processus sera créé. Au sein de ce nouveau processus enfant, émettez un avertissement pour empêcher la durée du test de dépasser sa limite de temps, puis exécutez le test défini par run().
Il existe également deux gestionnaires de signaux : le gestionnaire SIGCHLD sig_child(), qui est responsable de la collecte des processus enfants terminés et de l'exécution de la méthode post_run() de leur service ; le gestionnaire SIGUSR1 sig_usr1(), qui appelle simplement la méthode log_current_status() de tous les enregistreurs enregistrés, qui peut être utilisé pour obtenir l’état actuel de l’ensemble du système.
Bien entendu, cette architecture de surveillance ne fait rien de pratique. Mais d’abord, vous devez vérifier un service. La classe suivante vérifie si vous recevez une réponse « 200 Server OK » d'un serveur HTTP :
class HTTP_ServiceCheck extends ServiceCheck{
URL $ publique ;
fonction publique _ _construct($params){
foreach($params comme $k => $v) {
$k = "$k" ;
$this->$k = "$v";
}
}
fonction publique run(){
if(is_resource (@fopen($this->url, "r"))) {
retourner ServiceCheck :: SUCCÈS ;
}
autre {
renvoyer ServiceCheck :: FAILURE ;
}
}
}
Par rapport aux frameworks que vous avez construits auparavant, ce service est extrêmement simple et ne sera pas décrit en détail ici.
5. Exemple de processus ServiceLogger
Voici un exemple de processus ServiceLogger. Lorsqu'un service est en panne, il se charge d'envoyer un email à une personne d'astreinte :
la classe EmailMe_ServiceLogger implémente ServiceLogger {
fonction publique log_service_event (ServiceCheck $ service)
{
if($service->current_status ==ServiceCheck::FAILURE) {
$message = "Problème avec {$service->description()}rn";
mail( '[email protected]' , 'Service Event', $message);
if($service->consecutive_failures()> 5) {
mail( '[email protected]' , 'Service Event', $message);
}
}
}
fonction publique log_current_status(ServiceCheck$service){
retour;
}
}
S'il échoue cinq fois de suite, le processus envoie également un message à une adresse de sauvegarde. Notez qu’il n’implémente pas de méthode log_current_status() significative.
Chaque fois que vous modifiez l'état d'un service comme suit, vous devez implémenter un processus ServiceLogger qui écrit dans le journal des erreurs PHP :
class ErrorLog_ServiceLogger implémente ServiceLogger {
fonction publique log_service_event (ServiceCheck $ service)
{
if($service->current_status() !==$service->previous_status()) {
if($service->current_status() ===ServiceCheck::FAILURE) {
$statut = 'BAS';
}
autre {
$statut = 'UP';
}
error_log("{$service->description()} a changé le statut en $status");
}
}
fonction publique log_current_status(ServiceCheck$service)
{
error_log("{$service->description()} : $status");
}
}
La méthode log_current_status() signifie que si un processus envoie un signal SIGUSR1, il copiera son état actuel complet dans votre journal d'erreurs PHP.
Le moteur utilise un fichier de configuration comme suit :
<config>
<enregistreurs>
<enregistreur>
<id>journal des erreurs</id>
<classe>ErrorLog_ServiceLogger</classe>
</enregistreur>
<enregistreur>
<id>emailme</id>
<classe>EmailMe_ServiceLogger</classe>
</enregistreur>
</enregistreurs>
<services>
<service>
<classe>HTTP_ServiceCheck</classe>
<paramètres>
<description>Vérification HTTP OmniTI</description>
<url> http://www.omniti.com </url>
<délai d'attente>30</délai d'expiration>
<fréquence>900</fréquence>
</paramètres>
<enregistreurs>
<enregistreur>journal d'erreurs</enregistreur>
<enregistreur>emailme</enregistreur>
</enregistreurs>
</service>
<service>
<classe>HTTP_ServiceCheck</classe>
<paramètres>
<description>Vérification HTTP de la page d'accueil</description>
<url> http://www.schlossnagle.org/~george </url>
<délai d'attente>30</délai d'expiration>
<fréquence>3600</fréquence>
</paramètres>
<enregistreurs>
<enregistreur>journal d'erreurs</enregistreur>
</enregistreurs>
</service>
</services>
</config>
Une fois ce fichier XML transmis, le constructeur de ServiceCheckRunner instancie un programme de journalisation pour chaque journal spécifié. Ensuite, il instancie un objet ServiceCheck correspondant à chaque service spécifié.
Notez que le constructeur utilise la classe Reflection_Class pour implémenter des vérifications internes des classes de service et de journalisation - avant de tenter de les instancier. Bien que cela soit inutile, cela démontre bien l’utilisation de la nouvelle API Reflection dans PHP 5. En plus de ces classes, l'API Reflection fournit des classes pour implémenter l'inspection intrinsèque de presque toutes les entités internes (classe, méthode ou fonction) en PHP.
Afin d'utiliser le moteur que vous avez construit, vous avez encore besoin d'un code wrapper. Le chien de garde devrait vous empêcher d'essayer de le démarrer deux fois : vous n'avez pas besoin de créer deux messages pour chaque événement. Bien entendu, le moniteur devrait également recevoir certaines options, notamment :
Description de l'option
[-f] Un emplacement pour le fichier de configuration du moteur. La valeur par défaut est monitor.xml.
[-n] La taille du pool de processus enfant autorisée par le moteur. La valeur par défaut est 5.
[-d] Un indicateur qui désactive la fonctionnalité démon de ce moteur. Ceci est utile lorsque vous écrivez un processus de débogage ServiceLogger qui génère des informations sur stdout ou stderr.
Voici le script de surveillance final qui analyse les options, garantit l'exclusivité et exécute les contrôles de service :
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");
si(!$fp || !flock($fp, LOCK_EX | LOCK_NB)) {
fputs($stderr, "Échec de l'acquisition du verroun");
sortie;
}
si(!$args['d']) {
si(pcntl_fork()) {
sortie;
}
posix_setsid();
si(pcntl_fork()) {
sortie;
}
}
fwrite($fp, getmypid());
fflush($fp);
$engine = nouveau ServiceCheckRunner($args['f'], $args['n']);
$engine->loop();
Notez que cet exemple utilise la fonction getOptions() personnalisée.
Après avoir écrit un fichier de configuration approprié, vous pouvez démarrer le script comme suit :
> ./monitor.php -f /etc/monitor.xml
Cela protège et continue la surveillance jusqu'à ce que la machine soit arrêtée ou que le script soit tué.
Ce script est assez complexe, mais il reste encore quelques domaines faciles à améliorer, qui sont laissés en exercice au lecteur :
· Ajoutez un gestionnaire SIGHUP qui réanalyse le fichier de configuration afin que vous puissiez modifier la configuration sans démarrer le serveur.
· Écrivez un ServiceLogger qui peut se connecter à une base de données pour stocker les données de requête.
· Écrivez un programme frontal Web pour fournir une bonne interface graphique pour l'ensemble du système de surveillance.