الملخص: في هذه المقالة، دعونا نناقش العديد من التقنيات والاحتياطات لبناء محرك مراقبة أساسي من جانب الخادم يعتمد على لغة PHP، ونقدم تنفيذًا كاملاً للكود المصدري.
1. مشكلة تغيير دليل العمل
عند كتابة برنامج مراقبة، من الأفضل عادةً السماح له بتعيين دليل العمل الخاص به. بهذه الطريقة، إذا كنت تستخدم مسارًا نسبيًا لقراءة الملفات وكتابتها، فسوف يتعامل تلقائيًا مع الموقع الذي يتوقع المستخدم تخزين الملف فيه بناءً على الموقف. على الرغم من أنه من الممارسات الجيدة دائمًا تحديد المسارات المستخدمة في البرنامج، إلا أنها تفقد المرونة التي تستحقها. لذلك، فإن الطريقة الأكثر أمانًا لتغيير دليل العمل الخاص بك هي استخدام كل من chdir() و chroot().
يمكن استخدام chroot() في إصدارات PHP وCLI وCGI، ولكنه يتطلب تشغيل البرنامج بامتيازات الجذر. يقوم chroot() بالفعل بتغيير مسار العملية الحالية من الدليل الجذر إلى الدليل المحدد. يسمح هذا للعملية الحالية بتنفيذ الملفات الموجودة في هذا الدليل فقط. في كثير من الأحيان، يتم استخدام chroot() بواسطة الخوادم كـ "جهاز أمان" للتأكد من أن التعليمات البرمجية الضارة لا تقوم بتعديل الملفات خارج دليل معين. ضع في اعتبارك أنه على الرغم من أن chroot() يمنعك من الوصول إلى أي ملفات خارج الدليل الجديد، إلا أنه لا يزال من الممكن الوصول إلى أي موارد ملفات مفتوحة حاليًا. على سبيل المثال، يمكن للتعليمة البرمجية التالية فتح ملف سجل، واستدعاء chroot() والتبديل إلى دليل البيانات؛ ثم يظل بإمكانك تسجيل الدخول وفتح مورد الملف بنجاح:
<?php
$logfile = fopen("/var/log/chroot.log", "w");
chroot("/Users/george");
fputs($logfile, "مرحبًا من داخل The Chrootn");
>
إذا كان التطبيق لا يمكنه استخدام chroot()، فيمكنك الاتصال بـ chdir() لتعيين دليل العمل. يكون هذا مفيدًا، على سبيل المثال، عندما تحتاج التعليمات البرمجية إلى تحميل تعليمات برمجية محددة يمكن وضعها في أي مكان في النظام. لاحظ أن chdir() لا يوفر آلية أمان لمنع فتح الملفات بشكل غير مصرح به.
2. التخلي عن الامتيازات
عند كتابة برامج Unix، فإن الاحتياط الأمني الكلاسيكي هو جعلها تتخلى عن جميع الامتيازات غير الضرورية؛ وإلا فإن الحصول على امتيازات غير ضرورية يمكن أن يؤدي بسهولة إلى مشاكل غير ضرورية. في حالة وجود ثغرات أمنية في التعليمات البرمجية (أو PHP نفسها)، غالبًا ما يمكن تقليل الضرر عن طريق التأكد من تشغيل البرنامج الخفي كمستخدم أقل امتيازًا.
إحدى الطرق لتحقيق ذلك هي تنفيذ البرنامج الخفي كمستخدم لا يتمتع بأي امتيازات. ومع ذلك، لا يكون هذا كافيًا عادةً إذا كان البرنامج يحتاج في البداية إلى فتح الموارد التي ليس لدى المستخدمين غير المميزين إذن لفتحها (مثل ملفات السجل، وملفات البيانات، والمقابس، وما إلى ذلك).
إذا كنت تعمل كجذر، فيمكنك التخلي عن امتيازاتك بمساعدة الدالتين posix_setuid() وposiz_setgid(). يغير المثال التالي امتيازات البرنامج قيد التشغيل حاليًا إلى تلك التي يملكها المستخدم لا أحد:
$pw=posix_getpwnam('nobody');
posix_setuid($pw['uid']);
posix_setgid($pw['gid']);
تمامًا مثل chroot()، ستظل أي موارد مميزة تم فتحها قبل التنازل عن الامتيازات مفتوحة، لكن لا يمكن إنشاء موارد جديدة.
3. ضمان التفرد
الذي قد ترغب غالبًا في تحقيقه: تشغيل مثيل واحد فقط من البرنامج النصي في أي وقت. وهذا مهم بشكل خاص لحماية البرامج النصية، حيث أن تشغيلها في الخلفية يمكن أن يؤدي بسهولة إلى استدعاء مثيلات متعددة عن طريق الخطأ.
الأسلوب القياسي لضمان هذا التفرد هو جعل البرنامج النصي يقفل ملفًا محددًا (غالبًا ما يكون ملفًا مقفلاً، ويستخدم حصريًا) باستخدام قطيع (). إذا فشل القفل، فيجب أن يطبع البرنامج النصي خطأ ويخرج. هنا مثال:
$fp=fopen("/tmp/.lockfile"،"a")؛
إذا(!$fp || !flock($fp, LOCK_EX | LOCK_NB)) {
fputs(STDERR, "فشل الحصول على القفلn");
مخرج؛
}
/*تم القفل بنجاح لأداء العمل بأمان*/
لاحظ أن مناقشة آلية القفل تتضمن المزيد من المحتوى ولن يتم شرحها هنا.
4. بناء خدمة المراقبة
في هذا القسم، سوف نستخدم لغة PHP لكتابة محرك مراقبة أساسي. نظرًا لأنك لن تعرف مسبقًا كيفية تغييره، فيجب عليك جعل تنفيذه مرنًا وممكنًا.
يجب أن يكون المسجل قادرًا على دعم الفحص التعسفي للخدمة (على سبيل المثال، خدمات HTTP وFTP) وأن يكون قادرًا على تسجيل الأحداث بأي طريقة (عبر البريد الإلكتروني، أو الإخراج إلى ملف سجل، وما إلى ذلك). بالطبع تريد تشغيله كبرنامج خفي، لذلك يجب أن تطلب منه إخراج حالته الحالية الكاملة.
تحتاج الخدمة إلى تنفيذ الفئة المجردة التالية:
فئة مجردة ServiceCheck {
فشل ثابت = 0؛
النجاح الثابت = 1؛
مهلة $ محمية = 30؛
محمي $next_attempt;
protected $current_status = ServiceCheck::SUCCESS;
protected $previous_status = ServiceCheck::SUCCESS;
تردد $ المحمي = 30؛
وصف $ محمي؛
محمي $consecutive_failures = 0؛
محمي $status_time;
محمي $failure_time;
المحمي $loggers = array();
وظيفة عامة مجردة __construct($params);
الوظيفة العامة __call($name, $args)
{
إذا (إيسيت($هذا->$name)) {
إرجاع $this->$name;
}
}
الوظيفة العامة set_next_attempt()
{
$this->next_attempt = time() + $this->frequency;
}
تشغيل وظيفة مجردة عامة () ؛
الوظيفة العامة post_run($status)
{
إذا($status !== $this->current_status) {
$this->previous_status = $this->current_status;
}
إذا($status === self::FAILURE) {
إذا( $this->current_status === self::FAILURE ) {
$this->consecutive_failures++;
}
آخر {
$this->failure_time = time();
}
}
آخر {
$this->consecutive_failures = 0;
}
$this->status_time = time();
$this->current_status = $status;
$this->log_service_event();
}
الوظيفة العامة log_current_status()
{
foreach($this->المسجلون كـ $logger) {
$logger->log_current_status($this);
}
}
الوظيفة الخاصة log_service_event()
{
foreach($this->المسجلون كـ $logger) {
$logger->log_service_event($this);
}
}
الوظيفة العامة Register_logger (ServiceLogger $logger)
{
$this->loggers[] = $logger;
}
}
توفر طريقة التحميل الزائد __call() أعلاه وصولاً للقراءة فقط إلى معلمات كائن ServiceCheck:
· المهلة - كم من الوقت يمكن تعليق هذا الفحص قبل أن ينهي المحرك الفحص.
· next_attempt - المرة التالية التي تحاول فيها الاتصال بالخادم.
· الوضع الحالي - الوضع الحالي للخدمة: النجاح أو الفشل.
· Previous_status - الحالة قبل الوضع الحالي.
· التردد - عدد مرات التحقق من الخدمة.
· الوصف - وصف الخدمة.
· متتالية_فشل - عدد مرات فشل فحص الخدمة المتتالية منذ آخر نجاح.
· Status_time - آخر مرة تم فيها فحص الخدمة.
· وقت الفشل - إذا كانت الحالة "فشل"، فهو يمثل الوقت الذي حدث فيه الفشل.
تطبق هذه الفئة أيضًا نمط المراقب، مما يسمح للكائنات من النوع ServiceLogger بتسجيل نفسها ثم الاتصال بها عند استدعاء log_current_status() أو log_service_event().
الوظيفة الرئيسية المطبقة هنا هي run()، وهي المسؤولة عن تحديد كيفية إجراء الفحص. إذا كان الفحص ناجحًا، فيجب أن يُرجع SUCCESS؛ وإلا فإنه يجب أن يُرجع الفشل.
عندما يعود فحص الخدمة المحدد في run()، يتم استدعاء الأسلوب post_run(). وهو مسؤول عن تحديد حالة الكائن وتنفيذ التسجيل.
واجهة ServiceLogger: تحديد فئة السجل يحتاج فقط إلى تنفيذ طريقتين: log_service_event() وlog_current_status()، والتي يتم استدعاؤها عند إرجاع فحص التشغيل () وعند تنفيذ طلب الحالة العادية.
الواجهة هي كما يلي:
واجهة ServiceLogger {
الوظيفة العامة log_service_event(ServiceCheck$service);
الوظيفة العامة log_current_status(ServiceCheck$service);
}
وأخيرًا، عليك كتابة المحرك نفسه. الفكرة مشابهة لتلك المستخدمة عند كتابة البرنامج البسيط في القسم السابق: يجب على الخادم إنشاء عملية جديدة للتعامل مع كل فحص واستخدام معالج SIGCHLD للكشف عن القيمة المرجعة عند اكتمال الفحص. يجب أن يكون الحد الأقصى للعدد الذي يمكن التحقق منه في وقت واحد قابلاً للتكوين، وبالتالي منع الاستخدام المفرط لموارد النظام. سيتم تعريف كافة الخدمات والسجلات في ملف XML.
فيما يلي فئة ServiceCheckRunner التي تحدد هذا المحرك:
class ServiceCheckRunner {
خاص $num_children;
خدمات خاصة $ = array();
خاص $الأطفال = array();
الوظيفة العامة _ _construct($conf, $num_children)
{
$loggers = array();
$this->num_children = $num_children;
$conf = simplexml_load_file($conf);
foreach($conf->loggers->logger كـ $logger) {
$class = new Reflection_Class("$logger->class");
إذا($class->isInstantianable()) {
$loggers["$logger->id"] = $class->newInstance();
}
آخر {
fputs(STDERR, "لا يمكن إنشاء مثيل {$logger->class}.n");
مخرج؛
}
}
foreach($conf->services->service كـ $service) {
$class = new Reflection_Class("$service->class");
إذا($class->isInstantianable()) {
$item = $class->newInstance($service->params);
foreach($service->loggers->logger كـ $logger) {
$item->register_logger($loggers["$logger"]);
}
$this->services[] = $item;
}
آخر {
fputs(STDERR, "{$service->class} غير قابل للإنشاء الفوري.n");
مخرج؛
}
}
}
الوظيفة الخاصة next_attempt_sort($a, $b){
إذا($a->next_attempt() == $b->next_attempt()) {
العودة 0؛
}
العودة ($a->next_attempt() < $b->next_attempt()) -1: 1;
}
الوظيفة الخاصة التالية (){
usort($this->services, array($this, 'next_attempt_sort'));
إرجاع $this->services[0];
}
حلقة الوظيفة العامة (){
أعلن(القراد=1);
pcntl_signal(SIGCHLD, array($this, "sig_child"));
pcntl_signal(SIGUSR1, array($this, "sig_usr1"));
بينما (1) {
$الآن = الوقت();
إذا (عدد($هذا->الأطفال)< $هذا->num_children) {
$service = $this->next();
إذا($الآن < $service->next_attempt()) {
النوم(1);
يكمل؛
}
$service->set_next_attempt();
إذا($pid = pcntl_fork()) {
$this->children[$pid] = $service;
}
آخر {
pcntl_alarm($service->timeout());
خروج($service->run());
}
}
}
}
الوظيفة العامة log_current_status(){
foreach($this->الخدمات كخدمة $) {
$service->log_current_status();
}
}
وظيفة خاصة sig_child($signal){
$status = ServiceCheck::FAILURE;
pcntl_signal(SIGCHLD, array($this, "sig_child"));
بينما (($pid = pcntl_wait($status, WNOHANG)) > 0){
$service = $this->children[$pid];
unset($this->children[$pid]);
إذا (pcntl_wifexited($status) && pcntl_wexitstatus($status) ==ServiceCheck::SUCCESS)
{
$status = ServiceCheck::SUCCESS;
}
$service->post_run($status);
}
}
وظيفة خاصة sig_usr1($signal){
pcntl_signal(SIGUSR1, array($this, "sig_usr1"));
$this->log_current_status();
}
}
هذه فئة معقدة للغاية. يقوم منشئه بقراءة وتحليل ملف XML، وإنشاء جميع الخدمات المراد مراقبتها، وإنشاء مسجل لتسجيلها.
طريقة الحلقة () هي الطريقة الرئيسية في هذه الفئة. يقوم بتعيين معالج إشارة الطلب والتحقق من إمكانية إنشاء عملية فرعية جديدة. الآن، إذا كان الحدث التالي (مرتب حسب وقت المحاولة التالية CHUO) يعمل بشكل جيد، فسيتم إنشاء عملية جديدة. ضمن هذه العملية الفرعية الجديدة، قم بإصدار تحذير لمنع مدة الاختبار من تجاوز الحد الزمني لها، ثم قم بتنفيذ الاختبار المحدد بواسطة run().
يوجد أيضًا معالجان للإشارة: معالج SIGCHLD sig_child()، وهو المسؤول عن جمع العمليات الفرعية المنتهية وتنفيذ طريقة post_run() الخاصة بخدمتهم؛ ومعالج SIGUSR1 sig_usr1()، والذي يستدعي ببساطة جميع المسجلين المسجلين طريقة log_current_status()، والتي يمكن استخدامها للحصول على الوضع الحالي للنظام بأكمله.
وبطبيعة الحال، فإن بنية المراقبة هذه لا تفعل أي شيء عملي. ولكن أولا، تحتاج إلى التحقق من الخدمة. تتحقق الفئة التالية مما إذا كنت تحصل على استجابة "200 Server OK" من خادم HTTP:
class HTTP_ServiceCheck Extends ServiceCheck{
عنوان URL العام؛
الوظيفة العامة _ _construct($params){
foreach($params كـ $k => $v) {
$ك = "$ك";
$this->$k = "$v";
}
}
تشغيل الوظيفة العامة () {
إذا(is_resource(@fopen($this->url, "r"))) {
إرجاع ServiceCheck::SUCCESS;
}
آخر {
إرجاع ServiceCheck::FAILURE;
}
}
}
بالمقارنة مع الأطر التي قمت بإنشائها من قبل، فإن هذه الخدمة بسيطة للغاية ولن يتم وصفها بالتفصيل هنا.
5. نموذج لعملية ServiceLogger
فيما يلي نموذج لعملية ServiceLogger. عندما تكون الخدمة معطلة، تكون مسؤولة عن إرسال بريد إلكتروني إلى شخص تحت الطلب:
class EmailMe_ServiceLogger Implements ServiceLogger {
الوظيفة العامة log_service_event(ServiceCheck$service)
{
إذا($service->current_status ==ServiceCheck::FAILURE) {
$message = "مشكلة في {$service->description()}rn";
mail( '[email protected]' ، 'حدث الخدمة'، $message)؛
إذا($service->consecutive_failures()> 5) {
mail( '[email protected]' ، 'حدث الخدمة'، $message)؛
}
}
}
الوظيفة العامة log_current_status(ServiceCheck$service){
يعود؛
}
}
إذا فشلت خمس مرات متتالية، فسترسل العملية أيضًا رسالة إلى عنوان النسخ الاحتياطي. لاحظ أنه لا يطبق طريقة log_current_status() ذات معنى.
كلما قمت بتغيير حالة الخدمة على النحو التالي، يجب عليك تنفيذ عملية ServiceLogger التي تكتب إلى سجل أخطاء PHP
:
الوظيفة العامة log_service_event(ServiceCheck$service)
{
إذا($service->current_status() !==$service->previous_status()) {
إذا($service->current_status() ===ServiceCheck::FAILURE) {
الحالة $ = 'أسفل';
}
آخر {
الحالة $ = 'UP';
}
error_log("{$service->description()} تم تغيير الحالة إلى $status");
}
}
الوظيفة العامة log_current_status(ServiceCheck$service)
{
error_log("{$service->description()}: $status");
}
}
تعني طريقة log_current_status() أنه إذا أرسلت عملية ما إشارة SIGUSR1، فسوف تقوم بنسخ حالتها الحالية الكاملة إلى سجل أخطاء PHP الخاص بك.
يستخدم المحرك ملف التكوين كما يلي:
<config>
<المسجلون>
<المسجل>
<المعرف>سجل الأخطاء</المعرف>
<class>ErrorLog_ServiceLogger</class>
</المسجل>
<المسجل>
<المعرف>البريد الإلكتروني</المعرف>
<class>EmailMe_ServiceLogger</class>
</المسجل>
</المسجلون>
<خدمات>
<خدمة>
<فئة>HTTP_ServiceCheck</فئة>
<بارامس>
<الوصف>OmniTI HTTP Check</الوصف>
<url> http://www.omniti.com </url>
<مهلة>30</مهلة>
<التردد> 900</التردد>
</بارامز>
<المسجلون>
<المسجل>سجل الأخطاء</المسجل>
<المسجل>البريد الإلكتروني</المسجل>
</المسجلون>
</الخدمة>
<خدمة>
<فئة>HTTP_ServiceCheck</فئة>
<بارامس>
<وصف>فحص HTTP للصفحة الرئيسية</وصف>
<url> http://www.schlossnagle.org/~george </url>
<مهلة>30</مهلة>
<التردد>3600</التردد>
</بارامز>
<المسجلون>
<المسجل>سجل الأخطاء</المسجل>
</المسجلون>
</الخدمة>
</الخدمات>
</config>
عند تمرير ملف XML هذا، يقوم منشئ ServiceCheckRunner بإنشاء برنامج تسجيل لكل سجل محدد. ثم يقوم بإنشاء كائن ServiceCheck المطابق لكل خدمة محددة.
لاحظ أن المنشئ يستخدم فئة Reflection_Class لتنفيذ عمليات الفحص الداخلي للخدمة وفئات التسجيل - قبل محاولة إنشاء مثيل لها. على الرغم من أن هذا غير ضروري، إلا أنه يوضح بشكل جيد استخدام واجهة برمجة تطبيقات Reflection الجديدة في PHP 5. بالإضافة إلى هذه الفئات، توفر واجهة برمجة تطبيقات Reflection فئات لتنفيذ الفحص الجوهري لأي كيان داخلي تقريبًا (فئة أو طريقة أو وظيفة) في PHP.
من أجل استخدام المحرك الذي قمت بإنشائه، لا تزال بحاجة إلى بعض التعليمات البرمجية المجمعة. يجب أن تمنعك هيئة المراقبة من محاولة تشغيله مرتين - لا تحتاج إلى إنشاء رسالتين لكل حدث. وبطبيعة الحال، يجب أن تتلقى الشاشة أيضًا بعض الخيارات بما في ذلك:
وصف الخيار
[-f] الموقع الافتراضي لملف تكوين المحرك هو Monitor.xml.
[-n] حجم تجمع العمليات التابع الذي يسمح به المحرك هو 5.
[-d] علامة تعمل على تعطيل وظيفة البرنامج الخفي لهذا المحرك. يعد هذا مفيدًا عند كتابة عملية ServiceLogger لتصحيح الأخطاء والتي تقوم بإخراج المعلومات إلى stdout أو stderr.
هذا هو النص البرمجي النهائي للمراقبة الذي يوزع الخيارات ويضمن التفرد ويقوم بتشغيل فحوصات الخدمة:
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");
إذا(!$fp || !flock($fp, LOCK_EX | LOCK_NB)) {
fputs($stderr, "فشل الحصول على القفلn");
مخرج؛
}
إذا(!$args['d']) {
إذا (pcntl_fork()) {
مخرج؛
}
posix_setsid();
إذا (pcntl_fork()) {
مخرج؛
}
}
fwrite($fp, getmypid());
فلوش($fp);
$engine = new ServiceCheckRunner($args['f'], $args['n']);
$engine->loop();
لاحظ أن هذا المثال يستخدم وظيفة getOptions() المخصصة.
بعد كتابة ملف التكوين المناسب، يمكنك بدء البرنامج النصي على النحو التالي:
> ./monitor.php -f /etc/monitor.xml
وهذا يحمي ويستمر في المراقبة حتى يتم إيقاف تشغيل الجهاز أو إيقاف البرنامج النصي.
هذا البرنامج النصي معقد للغاية، ولكن لا تزال هناك بعض المناطق التي يمكن تحسينها بسهولة، والتي تُترك كتمرين للقارئ:
· أضف معالج SIGHUP الذي يعيد تحليل ملف التكوين بحيث يمكنك تغيير التكوين دون بدء تشغيل الخادم.
· اكتب ServiceLogger الذي يمكنه تسجيل الدخول إلى قاعدة بيانات لتخزين بيانات الاستعلام.
· كتابة برنامج واجهة الويب الأمامية لتوفير واجهة مستخدم رسومية جيدة لنظام المراقبة بأكمله.