Zusammenfassung: Lassen Sie uns in diesem Artikel die vielen Techniken und Vorsichtsmaßnahmen zum Aufbau einer grundlegenden serverseitigen Überwachungs-Engine basierend auf der PHP-Sprache diskutieren und eine vollständige Quellcode-Implementierung geben.
1. Das Problem beim Ändern des Arbeitsverzeichnisses
Wenn Sie ein Überwachungsprogramm schreiben, ist es normalerweise besser, es sein eigenes Arbeitsverzeichnis festlegen zu lassen. Wenn Sie auf diese Weise einen relativen Pfad zum Lesen und Schreiben von Dateien verwenden, wird automatisch der Speicherort verarbeitet, an dem der Benutzer die Datei je nach Situation erwartet. Obwohl es eine gute Praxis ist, die in einem Programm verwendeten Pfade immer zu begrenzen, geht dadurch die Flexibilität verloren, die es verdient. Daher ist die sicherste Möglichkeit, Ihr Arbeitsverzeichnis zu ändern, die Verwendung von chdir() und chroot().
chroot() kann in den CLI- und CGI-Versionen von PHP verwendet werden, erfordert jedoch, dass das Programm mit Root-Rechten ausgeführt wird. chroot() ändert tatsächlich den Pfad des aktuellen Prozesses vom Stammverzeichnis zum angegebenen Verzeichnis. Dadurch kann der aktuelle Prozess nur Dateien ausführen, die in diesem Verzeichnis vorhanden sind. Oft wird chroot() von Servern als „Sicherheitsgerät“ verwendet, um sicherzustellen, dass bösartiger Code keine Dateien außerhalb eines bestimmten Verzeichnisses verändert. Beachten Sie, dass chroot() zwar den Zugriff auf Dateien außerhalb Ihres neuen Verzeichnisses verhindert, der Zugriff auf alle derzeit geöffneten Dateiressourcen jedoch weiterhin möglich ist. Der folgende Code kann beispielsweise eine Protokolldatei öffnen, chroot() aufrufen und in ein Datenverzeichnis wechseln, um sich dann trotzdem erfolgreich anzumelden und die Dateiressource zu öffnen:
<?php
$logfile = fopen("/var/log/chroot.log", "w");
chroot("/Benutzer/george");
fputs($logfile, "Hallo aus der Chrootn");
?>
Wenn eine Anwendung chroot() nicht verwenden kann, können Sie chdir() aufrufen, um das Arbeitsverzeichnis festzulegen. Dies ist beispielsweise nützlich, wenn der Code einen bestimmten Code laden muss, der sich an einer beliebigen Stelle im System befinden kann. Beachten Sie, dass chdir() keinen Sicherheitsmechanismus bietet, um das unbefugte Öffnen von Dateien zu verhindern.
2. Privilegien aufgeben
Beim Schreiben von Unix-Daemons besteht eine klassische Sicherheitsmaßnahme darin, alle unnötigen Privilegien aufzugeben, da unnötige Privilegien sonst leicht zu unnötigen Problemen führen können. Im Falle von Schwachstellen im Code (oder in PHP selbst) kann der Schaden häufig minimiert werden, indem sichergestellt wird, dass ein Daemon als Benutzer mit den geringsten Privilegien ausgeführt wird.
Eine Möglichkeit, dies zu erreichen, besteht darin, den Daemon als unprivilegierter Benutzer auszuführen. Dies reicht jedoch normalerweise nicht aus, wenn das Programm zunächst Ressourcen öffnen muss, zu deren Öffnen unprivilegierte Benutzer keine Berechtigung haben (z. B. Protokolldateien, Datendateien, Sockets usw.).
Wenn Sie als Root ausgeführt werden, können Sie Ihre Berechtigungen mithilfe der Funktionen posix_setuid() und posiz_setgid() aufgeben. Das folgende Beispiel ändert die Berechtigungen des aktuell ausgeführten Programms in diejenigen, die dem Benutzer „nobody“ gehören:
$pw=posix_getpwnam('nobody');
posix_setuid($pw['uid']);
posix_setgid($pw['gid']);
Genau wie chroot() bleiben alle privilegierten Ressourcen, die vor dem Verzicht auf Privilegien geöffnet wurden, offen, es können jedoch keine neuen Ressourcen erstellt werden.
3. Garantieren Sie Exklusivität.
Möglicherweise möchten Sie häufig erreichen, dass jeweils nur eine Instanz eines Skripts ausgeführt wird. Dies ist besonders wichtig, um Skripte zu schützen, da deren Ausführung im Hintergrund leicht dazu führen kann, dass versehentlich mehrere Instanzen aufgerufen werden.
Die Standardtechnik zur Gewährleistung dieser Exklusivität besteht darin, das Skript mithilfe von flock() eine bestimmte Datei (häufig eine gesperrte Datei, die ausschließlich verwendet wird) sperren zu lassen. Wenn die Sperre fehlschlägt, sollte das Skript eine Fehlermeldung ausgeben und beendet werden. Hier ist ein Beispiel:
$fp=fopen("/tmp/.lockfile","a");
if(!$fp || !flock($fp, LOCK_EX | LOCK_NB)) {
fputs(STDERR, „Fehler beim Erwerb der Sperren“);
Ausfahrt;
}
/*Erfolgreich gesperrt, um Arbeiten sicher auszuführen*/
Beachten Sie, dass die Diskussion des Sperrmechanismus mehr Inhalt umfasst und hier nicht erläutert wird.
4. Aufbau des Überwachungsdienstes
In diesem Abschnitt verwenden wir PHP, um eine grundlegende Überwachungs-Engine zu schreiben. Da Sie nicht im Voraus wissen, wie Sie es ändern können, sollten Sie die Implementierung flexibel und möglich gestalten.
Der Logger sollte in der Lage sein, beliebige Dienstinspektionen (z. B. HTTP- und FTP-Dienste) zu unterstützen und Ereignisse auf beliebige Weise zu protokollieren (per E-Mail, Ausgabe in eine Protokolldatei usw.). Natürlich möchten Sie, dass es als Daemon läuft. Daher sollten Sie es bitten, seinen vollständigen aktuellen Status auszugeben.
Ein Dienst muss die folgende abstrakte Klasse implementieren:
abstrakte Klasse ServiceCheck {
const FAILURE = 0;
const ERFOLGREICH = 1;
geschützt $timeout = 30;
protected $next_attempt;
protected $current_status = ServiceCheck::SUCCESS;
protected $ previous_status = ServiceCheck::SUCCESS;
geschützte $Frequenz = 30;
geschützte $beschreibung;
protected $consecutive_failures = 0;
geschützt $status_time;
protected $failure_time;
protected $loggers = array();
abstrakte öffentliche Funktion __construct($params);
öffentliche Funktion __call($name, $args)
{
if(isset($this->$name)) {
return $this->$name;
}
}
öffentliche Funktion set_next_attempt()
{
$this->next_attempt = time() + $this->frequenz;
}
öffentliche abstrakte Funktion run();
öffentliche Funktion 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++;
}
anders {
$this->failure_time = time();
}
}
anders {
$this->consecutive_failures = 0;
}
$this->status_time = time();
$this->current_status = $status;
$this->log_service_event();
}
öffentliche Funktion log_current_status()
{
foreach($this->loggers as $logger) {
$logger->log_current_status($this);
}
}
private Funktion log_service_event()
{
foreach($this->loggers as $logger) {
$logger->log_service_event($this);
}
}
öffentliche Funktion register_logger(ServiceLogger $logger)
{
$this->loggers[] = $logger;
}
}
Die überladene Methode __call() oben bietet schreibgeschützten Zugriff auf die Parameter eines ServiceCheck-Objekts:
· Timeout – wie lange diese Prüfung angehalten werden kann, bevor die Engine die Prüfung beendet.
· next_attempt – Der nächste Versuch, eine Verbindung zum Server herzustellen.
· current_status – Der aktuelle Status des Dienstes: ERFOLGREICH oder FEHLER.
· previous_status – der Status vor dem aktuellen Status.
· Häufigkeit – wie oft der Dienst überprüft werden soll.
· Beschreibung – Leistungsbeschreibung.
· fortlaufende_Fehler – Die Anzahl aufeinanderfolgender Dienstüberprüfungsfehler seit dem letzten Erfolg.
· status_time – Der letzte Zeitpunkt, zu dem der Dienst überprüft wurde.
· fail_time – Wenn der Status FAILED ist, stellt er die Zeit dar, zu der der Fehler aufgetreten ist.
Diese Klasse implementiert auch das Observer-Muster und ermöglicht es Objekten vom Typ ServiceLogger, sich selbst zu registrieren und es dann aufzurufen, wenn log_current_status() oder log_service_event() aufgerufen wird.
Die hier implementierte Schlüsselfunktion ist run(), die dafür verantwortlich ist, zu definieren, wie die Prüfung durchgeführt werden soll. Wenn die Prüfung erfolgreich ist, sollte sie SUCCESS zurückgeben; andernfalls sollte sie FAILURE zurückgeben.
Wenn die in run() definierte Dienstprüfung zurückkehrt, wird die Methode post_run() aufgerufen. Es ist dafür verantwortlich, den Status des Objekts festzulegen und die Protokollierung zu implementieren.
ServiceLogger-Schnittstelle: Durch die Angabe einer Protokollklasse müssen lediglich zwei Methoden implementiert werden: log_service_event() und log_current_status(), die aufgerufen werden, wenn eine run()-Prüfung zurückgegeben wird und wenn eine normale Statusanforderung implementiert wird.
Die Schnittstelle ist wie folgt:
Schnittstelle ServiceLogger {
öffentliche Funktion log_service_event(ServiceCheck$service);
öffentliche Funktion log_current_status(ServiceCheck$service);
}
Schließlich müssen Sie die Engine selbst schreiben. Die Idee ähnelt der beim Schreiben des einfachen Programms im vorherigen Abschnitt: Der Server sollte einen neuen Prozess erstellen, um jede Prüfung abzuwickeln, und einen SIGCHLD-Handler verwenden, um den Rückgabewert zu erkennen, wenn die Prüfung abgeschlossen ist. Die maximale Anzahl, die gleichzeitig überprüft werden kann, sollte konfigurierbar sein, um eine übermäßige Nutzung der Systemressourcen zu verhindern. Alle Dienste und Protokolle werden in einer XML-Datei definiert.
Hier ist die ServiceCheckRunner-Klasse, die diese Engine definiert:
class ServiceCheckRunner {
privat $num_children;
private $services = array();
private $children = array();
öffentliche Funktion _ _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();
}
anders {
fputs(STDERR, "{$logger->class} kann nicht instanziiert werden.n");
Ausfahrt;
}
}
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;
}
anders {
fputs(STDERR, "{$service->class} ist nicht instanziierbar.n");
Ausfahrt;
}
}
}
private Funktion next_attempt_sort($a, $b){
if($a->next_attempt() == $b->next_attempt()) {
0 zurückgeben;
}
return ($a->next_attempt() < $b->next_attempt())?
}
private Funktion next(){
usort($this->services, array($this, 'next_attempt_sort'));
return $this->services[0];
}
öffentliche Funktionsschleife(){
deklarieren(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()) {
Schlaf(1);
weitermachen;
}
$service->set_next_attempt();
if($pid = pcntl_fork()) {
$this->children[$pid] = $service;
}
anders {
pcntl_alarm($service->timeout());
exit($service->run());
}
}
}
}
öffentliche Funktion log_current_status(){
foreach($this->services as $service) {
$service->log_current_status();
}
}
private Funktion 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 Funktion sig_usr1($signal){
pcntl_signal(SIGUSR1, array($this, "sig_usr1"));
$this->log_current_status();
}
}
Dies ist eine sehr komplexe Klasse. Sein Konstruktor liest und analysiert eine XML-Datei, erstellt alle zu überwachenden Dienste und erstellt einen Logger, um sie aufzuzeichnen.
Die Methode loop() ist die Hauptmethode in dieser Klasse. Es legt den Signalhandler der Anfrage fest und prüft, ob ein neuer untergeordneter Prozess erstellt werden kann. Wenn nun das nächste Ereignis (geordnet nach der nächsten_Versuchszeit CHUO) gut verläuft, wird ein neuer Prozess erstellt. Geben Sie innerhalb dieses neuen untergeordneten Prozesses eine Warnung aus, um zu verhindern, dass die Testdauer ihr Zeitlimit überschreitet, und führen Sie dann den durch run() definierten Test aus.
Es gibt auch zwei Signalhandler: den SIGCHLD-Handler sig_child(), der für das Sammeln beendeter untergeordneter Prozesse und die Ausführung der post_run()-Methode ihres Dienstes verantwortlich ist; den SIGUSR1-Handler sig_usr1(), der einfach die log_current_status()-Methode aller registrierten Logger aufruft kann verwendet werden, um den aktuellen Status des gesamten Systems abzurufen.
Natürlich bringt diese Überwachungsarchitektur nichts Praktisches. Aber zuerst müssen Sie sich einen Dienst ansehen. Die folgende Klasse prüft, ob Sie eine „200 Server OK“-Antwort von einem HTTP-Server erhalten:
Klasse HTTP_ServiceCheck erweitert ServiceCheck{
öffentliche $URL;
öffentliche Funktion _ _construct($params){
foreach($params as $k => $v) {
$k = "$k";
$this->$k = "$v";
}
}
öffentliche Funktion run(){
if(is_resource(@fopen($this->url, "r"))) {
return ServiceCheck::SUCCESS;
}
anders {
return ServiceCheck::FAILURE;
}
}
}
Im Vergleich zu den Frameworks, die Sie zuvor erstellt haben, ist dieser Dienst äußerst einfach und wird hier nicht im Detail beschrieben.
5. Beispiel-ServiceLogger-Prozess
Das Folgende ist ein Beispiel-ServiceLogger-Prozess. Wenn ein Dienst ausfällt, ist er dafür verantwortlich, eine E-Mail an eine Bereitschaftsperson zu senden:
Die Klasse EmailMe_ServiceLogger implementiert ServiceLogger {
öffentliche Funktion log_service_event(ServiceCheck$service)
{
if($service->current_status ==ServiceCheck::FAILURE) {
$message = "Problem mit {$service->description()}rn";
mail( '[email protected]' , 'Service Event', $message);
if($service->consecutive_failures()> 5) {
mail( '[email protected]' , 'Service Event', $message);
}
}
}
öffentliche Funktion log_current_status(ServiceCheck$service){
zurückkehren;
}
}
Wenn es fünfmal hintereinander fehlschlägt, sendet der Prozess auch eine Nachricht an eine Backup-Adresse. Beachten Sie, dass keine sinnvolle log_current_status()-Methode implementiert wird.
Wenn Sie den Status eines Dienstes wie folgt ändern, sollten Sie einen ServiceLogger-Prozess implementieren, der in das PHP-Fehlerprotokoll schreibt:
class ErrorLog_ServiceLogger implementiert ServiceLogger {
öffentliche Funktion log_service_event(ServiceCheck$service)
{
if($service->current_status() !==$service-> previous_status()) {
if($service->current_status() ===ServiceCheck::FAILURE) {
$status = 'DOWN';
}
anders {
$status = 'UP';
}
error_log("{$service->description()} hat den Status in $status geändert");
}
}
öffentliche Funktion log_current_status(ServiceCheck$service)
{
error_log("{$service->description()}: $status");
}
}
Die Methode log_current_status() bedeutet, dass ein Prozess, wenn er ein SIGUSR1-Signal sendet, seinen vollständigen aktuellen Status in Ihr PHP-Fehlerprotokoll kopiert.
Die Engine verwendet eine Konfigurationsdatei wie folgt:
<config>
<Logger>
<Logger>
<id>errorlog</id>
<class>ErrorLog_ServiceLogger</class>
</logger>
<Logger>
<id>emailme</id>
<class>EmailMe_ServiceLogger</class>
</logger>
</loggers>
<Dienstleistungen>
<Dienst>
<class>HTTP_ServiceCheck</class>
<params>
<Beschreibung>OmniTI HTTP Check</Beschreibung>
<url> http://www.omniti.com </url>
<Zeitüberschreitung>30</Zeitüberschreitung>
<Frequenz>900</Frequenz>
</params>
<Logger>
<logger>errorlog</logger>
<logger>emailme</logger>
</loggers>
</Dienstleistung>
<Dienst>
<class>HTTP_ServiceCheck</class>
<params>
<description>Home Page HTTP Check</description>
<url> http://www.schlossnagle.org/~george </url>
<Zeitüberschreitung>30</Zeitüberschreitung>
<Frequenz>3600</Frequenz>
</params>
<Logger>
<logger>errorlog</logger>
</loggers>
</Dienstleistung>
</Dienstleistungen>
</config>
Wenn diese XML-Datei übergeben wird, instanziiert der Konstruktor von ServiceCheckRunner ein Protokollierungsprogramm für jedes angegebene Protokoll. Anschließend instanziiert es ein ServiceCheck-Objekt, das jedem angegebenen Dienst entspricht.
Beachten Sie, dass der Konstruktor die Reflection_Class-Klasse verwendet, um interne Prüfungen der Dienst- und Protokollierungsklassen zu implementieren – bevor Sie versuchen, sie zu instanziieren. Obwohl dies unnötig ist, demonstriert es doch sehr schön die Verwendung der neuen Reflection API in PHP 5. Zusätzlich zu diesen Klassen bietet die Reflection API Klassen zur Implementierung einer intrinsischen Inspektion fast aller internen Entitäten (Klasse, Methode oder Funktion) in PHP.
Um die von Ihnen erstellte Engine verwenden zu können, benötigen Sie noch Wrapper-Code. Der Watchdog sollte verhindern, dass Sie versuchen, ihn zweimal zu starten – Sie müssen nicht für jedes Ereignis zwei Nachrichten erstellen. Natürlich sollte der Monitor auch einige Optionen erhalten, darunter:
Optionsbeschreibung
[-f] Ein Speicherort für die Engine-Konfigurationsdatei. Der Standardwert ist monitor.xml.
[-n] Die von der Engine zugelassene Größe des untergeordneten Prozesspools. Der Standardwert ist 5.
[-d] Ein Flag, das die Daemon-Funktionalität dieser Engine deaktiviert. Dies ist nützlich, wenn Sie einen Debug-ServiceLogger-Prozess schreiben, der Informationen an stdout oder stderr ausgibt.
Hier ist das endgültige Watchdog-Skript, das Optionen analysiert, Exklusivität gewährleistet und Dienstprüfungen durchführt:
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, „Fehler beim Erwerb der Sperren“);
Ausfahrt;
}
if(!$args['d']) {
if(pcntl_fork()) {
Ausfahrt;
}
posix_setsid();
if(pcntl_fork()) {
Ausfahrt;
}
}
fwrite($fp, getmypid());
flush($fp);
$engine = new ServiceCheckRunner($args['f'], $args['n']);
$engine->loop();
Beachten Sie, dass in diesem Beispiel die angepasste Funktion getOptions() verwendet wird.
Nachdem Sie eine entsprechende Konfigurationsdatei geschrieben haben, können Sie das Skript wie folgt starten:
> ./monitor.php -f /etc/monitor.xml
Dies schützt und setzt die Überwachung fort, bis die Maschine heruntergefahren oder das Skript beendet wird.
Dieses Skript ist ziemlich komplex, aber es gibt dennoch einige leicht zu verbessernde Bereiche, die dem Leser als Übung überlassen bleiben:
· Fügen Sie einen SIGHUP-Handler hinzu, der die Konfigurationsdatei erneut analysiert, sodass Sie die Konfiguration ändern können, ohne den Server zu starten.
· Schreiben Sie einen ServiceLogger, der sich bei einer Datenbank anmelden kann, um Abfragedaten zu speichern.
· Schreiben Sie ein Web-Frontend-Programm, um eine gute GUI für das gesamte Überwachungssystem bereitzustellen.