Abstrak: Pada artikel ini, mari kita bahas berbagai teknik dan tindakan pencegahan untuk membangun mesin pemantauan sisi server dasar berdasarkan bahasa PHP, dan memberikan implementasi kode sumber yang lengkap.
1. Masalah perubahan direktori kerja
Saat Anda menulis program pemantauan, biasanya lebih baik membiarkannya mengatur direktori kerjanya sendiri. Dengan cara ini, jika Anda menggunakan jalur relatif untuk membaca dan menulis file, maka secara otomatis akan menangani lokasi di mana pengguna mengharapkan file tersebut disimpan berdasarkan situasinya. Meskipun merupakan praktik yang baik untuk selalu membatasi jalur yang digunakan dalam suatu program; namun, hal ini kehilangan fleksibilitas yang layak. Oleh karena itu, cara paling aman untuk mengubah direktori kerja Anda adalah dengan menggunakan chdir() dan chroot().
chroot() dapat digunakan pada PHP versi CLI dan CGI, namun memerlukan program untuk dijalankan dengan hak akses root. chroot() sebenarnya mengubah jalur proses saat ini dari direktori root ke direktori yang ditentukan. Hal ini memungkinkan proses saat ini untuk mengeksekusi hanya file yang ada di direktori tersebut. Seringkali, chroot() digunakan oleh server sebagai "perangkat keamanan" untuk memastikan bahwa kode berbahaya tidak mengubah file di luar direktori tertentu. Ingatlah bahwa meskipun chroot() mencegah Anda mengakses file apa pun di luar direktori baru Anda, sumber daya file apa pun yang sedang terbuka masih dapat diakses. Misalnya, kode berikut dapat membuka file log, memanggil chroot() dan beralih ke direktori data; kemudian, masih berhasil masuk dan membuka sumber file:
<?php
$logfile = fopen("/var/log/chroot.log", "w");
chroot("/Pengguna/george");
fputs($logfile, "Halo Dari Dalam Chrootn");
?>
Jika aplikasi tidak dapat menggunakan chroot(), Anda dapat memanggil chdir() untuk mengatur direktori kerja. Hal ini berguna, misalnya, ketika kode perlu memuat kode tertentu yang dapat ditempatkan dimana saja dalam sistem. Perhatikan bahwa chdir() tidak menyediakan mekanisme keamanan untuk mencegah pembukaan file tanpa izin.
2. Menyerahkan hak istimewa
Saat menulis daemon Unix, tindakan pencegahan keamanan klasik adalah dengan meminta daemon tersebut melepaskan semua hak istimewa yang tidak perlu, jika tidak, memiliki hak istimewa yang tidak perlu dapat dengan mudah menimbulkan masalah yang tidak perlu; Jika terjadi kerentanan dalam kode (atau PHP itu sendiri), kerusakan seringkali dapat diminimalkan dengan memastikan bahwa daemon berjalan sebagai pengguna yang paling tidak memiliki hak istimewa.
Salah satu cara untuk mencapai hal ini adalah dengan mengeksekusi daemon sebagai pengguna yang tidak memiliki hak istimewa. Namun, ini biasanya tidak cukup jika program pada awalnya perlu membuka sumber daya yang izinnya tidak dapat dibuka oleh pengguna yang tidak memiliki hak istimewa (seperti file log, file data, soket, dll.).
Jika Anda menjalankan sebagai root, Anda dapat melepaskan hak istimewa Anda dengan bantuan fungsi posix_setuid() dan posiz_setgid(). Contoh berikut mengubah hak istimewa program yang sedang berjalan menjadi hak yang dimiliki oleh pengguna siapa pun:
$pw=posix_getpwnam('nobody');
posix_setuid($pw['uid']);
posix_setgid($pw['gid']);
Sama seperti chroot(), sumber daya istimewa apa pun yang dibuka sebelum melepaskan hak istimewa akan tetap terbuka, tetapi sumber daya baru tidak dapat dibuat.
3. Menjamin eksklusivitas
Anda mungkin sering ingin mencapai: hanya satu skrip yang berjalan pada suatu waktu. Hal ini sangat penting untuk melindungi skrip, karena menjalankannya di latar belakang dapat dengan mudah menyebabkan pemanggilan beberapa instance secara tidak sengaja.
Teknik standar untuk memastikan eksklusivitas ini adalah dengan membuat skrip mengunci file tertentu (seringkali file terkunci, dan digunakan secara eksklusif) dengan menggunakan kawanan(). Jika kunci gagal, skrip akan mencetak kesalahan dan keluar. Berikut ini contohnya:
$fp=fopen("/tmp/.lockfile","a");
if(!$fp || !flock($fp, LOCK_EX | LOCK_NB)) {
fputs(STDERR, "Gagal memperoleh kuncin");
KELUAR;
}
/*Berhasil dikunci untuk melakukan pekerjaan dengan aman*/
Perhatikan bahwa pembahasan mekanisme kunci melibatkan lebih banyak konten dan tidak akan dijelaskan di sini.
4. Membangun Layanan Monitoring
Pada bagian ini, kita akan menggunakan PHP untuk menulis mesin monitoring dasar. Karena Anda tidak mengetahui sebelumnya cara mengubahnya, Anda harus membuat penerapannya fleksibel dan memungkinkan.
Logger harus dapat mendukung pemeriksaan layanan sewenang-wenang (misalnya, layanan HTTP dan FTP) dan dapat mencatat peristiwa dengan cara apa pun (melalui email, output ke file log, dll.). Tentu saja Anda ingin menjalankannya sebagai daemon, oleh karena itu, Anda harus memintanya untuk menampilkan status lengkapnya saat ini;
Sebuah layanan perlu mengimplementasikan kelas abstrak berikut:
kelas abstrak ServiceCheck {
const KEGAGALAN = 0;
const KEBERHASILAN = 1;
dilindungi $batas waktu = 30;
dilindungi $next_attempt;
dilindungi $current_status = ServiceCheck::SUKSES;
dilindungi $previous_status = ServiceCheck::SUKSES;
dilindungi $frekuensi = 30;
dilindungi $deskripsi;
dilindungi $kegagalan_berturut-turut = 0;
dilindungi $status_time;
dilindungi $failure_time;
dilindungi $logger = array();
fungsi publik abstrak __construct($params);
fungsi publik __panggilan($nama, $args)
{
if(isset($ini->$nama)) {
kembalikan $ini->$nama;
}
}
fungsi publik set_next_attempt()
{
$ini->next_attempt = waktu() + $ini->frekuensi;
}
fungsi abstrak publik run();
fungsi publik post_run($status)
{
if($status !== $ini->status_saat ini) {
$ini->status_sebelumnya = $ini->status_saat ini;
}
if($status === diri::GAGAL) {
jika( $ini->status_saat ini === diri::GAGAL ) {
$ini->kegagalan_berturut-turut++;
}
kalau tidak {
$ini->failure_time = waktu();
}
}
kalau tidak {
$ini->kegagalan_berturut-turut = 0;
}
$ini->status_time = waktu();
$ini->status_saat ini = $status;
$ini->log_service_event();
}
fungsi publik log_current_status()
{
foreach($this->logger sebagai $logger) {
$logger->log_current_status($ini);
}
}
fungsi pribadi log_service_event()
{
foreach($this->logger sebagai $logger) {
$logger->log_service_event($ini);
}
}
fungsi publik register_logger(ServiceLogger $logger)
{
$ini->logger[] = $logger;
}
}
Metode __call() yang kelebihan beban di atas menyediakan akses read-only ke parameter objek ServiceCheck:
· timeout - berapa lama pemeriksaan ini dapat ditangguhkan sebelum mesin menghentikan pemeriksaan.
· next_attempt - Kali berikutnya mencoba menyambung ke server.
· current_status - Status layanan saat ini: SUKSES atau KEGAGALAN.
· previous_status - status sebelum status saat ini.
· frekuensi - seberapa sering memeriksa layanan.
· deskripsi - deskripsi layanan.
· berturut-turut_failures - Jumlah kegagalan pemeriksaan layanan berturut-turut sejak keberhasilan terakhir.
· status_time - Terakhir kali layanan diperiksa.
· waktu_kegagalan - Jika statusnya GAGAL, ini menunjukkan waktu terjadinya kegagalan.
Kelas ini juga mengimplementasikan pola Observer, yang memungkinkan objek bertipe ServiceLogger mendaftarkan dirinya sendiri dan kemudian memanggilnya ketika log_current_status() atau log_service_event() dipanggil.
Fungsi utama yang diterapkan di sini adalah run(), yang bertanggung jawab untuk menentukan bagaimana pemeriksaan harus dilakukan. Jika pemeriksaan berhasil, maka harus mengembalikan SUCCESS; jika tidak maka akan mengembalikan FAILURE.
Ketika pemeriksaan layanan yang ditentukan dalam run() kembali, metode post_run() dipanggil. Bertanggung jawab untuk mengatur status objek dan mengimplementasikan logging.
Antarmuka ServiceLogger: Menentukan kelas log hanya perlu mengimplementasikan dua metode: log_service_event() dan log_current_status(), yang dipanggil ketika pemeriksaan run() kembali dan ketika permintaan status normal diterapkan.
Antarmukanya adalah sebagai berikut:
antarmuka ServiceLogger {
fungsi publik log_service_event(ServiceCheck$service);
fungsi publik log_current_status(ServiceCheck$service);
}
Terakhir, Anda perlu menulis mesinnya sendiri. Idenya mirip dengan yang digunakan saat menulis program sederhana di bagian sebelumnya: server harus membuat proses baru untuk menangani setiap pemeriksaan dan menggunakan pengendali SIGCHLD untuk mendeteksi nilai yang dikembalikan ketika pemeriksaan selesai. Jumlah maksimum yang dapat diperiksa secara bersamaan harus dapat dikonfigurasi, sehingga mencegah penggunaan sumber daya sistem secara berlebihan. Semua layanan dan log akan ditentukan dalam file XML.
Berikut adalah kelas ServiceCheckRunner yang mendefinisikan mesin ini:
class ServiceCheckRunner {
pribadi $num_children;
pribadi $layanan = array();
pribadi $anak-anak = array();
fungsi publik _ _construct($conf, $num_children)
{
$logger = array();
$ini->angka_anak = $angka_anak;
$conf = simplexml_load_file($conf);
foreach($conf->loggers->logger sebagai $logger) {
$kelas = new Reflection_Class("$logger->kelas");
if($class->isInstantiable()) {
$logger["$logger->id"] = $class->newInstance();
}
kalau tidak {
fputs(STDERR, "{$logger->class} tidak dapat dipakai.n");
KELUAR;
}
}
foreach($conf->layanan->layanan sebagai $layanan) {
$kelas = kelas_Refleksi baru("$layanan->kelas");
if($class->isInstantiable()) {
$item = $class->newInstance($service->params);
foreach($service->loggers->logger sebagai $logger) {
$item->register_logger($logger["$logger"]);
}
$ini->layanan[] = $item;
}
kalau tidak {
fputs(STDERR, "{$service->class} tidak dapat dipakai.n");
KELUAR;
}
}
}
fungsi pribadi next_attempt_sort($a, $b){
if($a->next_attempt() == $b->next_attempt()) {
kembali 0;
}
kembali ($a->next_attempt() < $b->next_attempt())?
}
fungsi pribadi selanjutnya(){
usort($ini->layanan, array($ini, 'next_attempt_sort'));
kembalikan $ini->layanan[0];
}
perulangan fungsi publik(){
menyatakan(centang=1);
pcntl_signal(SIGCHLD, array($ini, "sig_child"));
pcntl_signal(SIGUSR1, array($ini, "sig_usr1"));
sementara(1) {
$sekarang = waktu();
if(count($this->children)< $this->num_children) {
$layanan = $ini->berikutnya();
if($sekarang < $layanan->next_attempt()) {
tidur(1);
melanjutkan;
}
$layanan->set_next_attempt();
jika($pid = pcntl_fork()) {
$ini->anak-anak[$pid] = $layanan;
}
kalau tidak {
pcntl_alarm($layanan->batas waktu());
keluar($layanan->jalankan());
}
}
}
}
fungsi publik log_current_status(){
foreach($ini->layanan sebagai $layanan) {
$layanan->log_current_status();
}
}
fungsi pribadi sig_child($sinyal){
$status = ServiceCheck::GAGAL;
pcntl_signal(SIGCHLD, array($ini, "sig_child"));
while(($pid = pcntl_wait($status, WNOHANG)) > 0){
$layanan = $ini->anak-anak[$pid];
tidak disetel($ini->anak-anak[$pid]);
if(pcntl_wifexited($status) && pcntl_wexitstatus($status) ==ServiceCheck::SUKSES)
{
$status = ServiceCheck::SUKSES;
}
$layanan->post_run($status);
}
}
fungsi pribadi sig_usr1($sinyal){
pcntl_signal(SIGUSR1, array($ini, "sig_usr1"));
$ini->log_current_status();
}
}
Ini adalah kelas yang sangat kompleks. Konstruktornya membaca dan mem-parsing file XML, membuat semua layanan untuk dipantau, dan membuat logger untuk mencatatnya.
Metode loop() adalah metode utama di kelas ini. Ini menetapkan penangan sinyal permintaan dan memeriksa apakah proses anak baru dapat dibuat. Sekarang, jika event berikutnya (diurutkan berdasarkan next_attempt time CHUO) berjalan dengan baik, proses baru akan dibuat. Dalam proses anak baru ini, keluarkan peringatan untuk mencegah durasi pengujian melebihi batas waktunya, lalu jalankan pengujian yang ditentukan oleh run().
Ada juga dua penangan sinyal: penangan SIGCHLD sig_child(), yang bertanggung jawab untuk mengumpulkan proses anak yang dihentikan dan mengeksekusi metode post_run() layanannya; penangan SIGUSR1 sig_usr1(), yang memanggil semua metode log_current_status() yang terdaftar, yang mana dapat digunakan untuk mendapatkan status terkini dari seluruh sistem.
Tentu saja, arsitektur pengawasan ini tidak memberikan manfaat praktis. Tapi pertama-tama, Anda perlu memeriksa layanannya. Kelas berikut memeriksa apakah Anda mendapatkan respons "200 Server OK" dari server HTTP:
class HTTP_ServiceCheck extends ServiceCheck{
publik $url;
fungsi publik _ _construct($params){
foreach($params sebagai $k => $v) {
$k = "$k";
$ini->$k = "$v";
}
}
fungsi publik dijalankan(){
if(is_resource(@fopen($ini->url, "r"))) {
kembali ServiceCheck::SUKSES;
}
kalau tidak {
kembalikan ServiceCheck::FAILURE;
}
}
}
Dibandingkan dengan kerangka kerja yang Anda buat sebelumnya, layanan ini sangat sederhana dan tidak akan dijelaskan secara rinci di sini.
5. Contoh proses ServiceLogger
Berikut ini adalah contoh proses ServiceLogger. Ketika suatu layanan tidak aktif, ia bertanggung jawab untuk mengirimkan email ke petugas panggilan:
class EmailMe_ServiceLogger mengimplementasikan ServiceLogger {
fungsi publik log_service_event(ServiceCheck$service)
{
if($service->current_status ==ServiceCheck::FAILURE) {
$message = "Masalah dengan{$service->description()}rn";
mail( '[email protected]' , 'Acara Layanan', $message);
if($service->kegagalan_berturut-turut()> 5) {
mail( '[email protected]' , 'Acara Layanan', $message);
}
}
}
fungsi publik log_current_status(ServiceCheck$service){
kembali;
}
}
Jika gagal lima kali berturut-turut, proses juga mengirimkan pesan ke alamat cadangan. Perhatikan bahwa ini tidak mengimplementasikan metode log_current_status() yang berarti.
Setiap kali Anda mengubah status layanan sebagai berikut, Anda harus menerapkan proses ServiceLogger yang menulis ke log kesalahan PHP:
class ErrorLog_ServiceLogger mengimplementasikan ServiceLogger {
fungsi publik log_service_event(ServiceCheck$service)
{
if($layanan->status_saat ini() !==$layanan->status_sebelumnya()) {
if($service->current_status() ===ServiceCheck::FAILURE) {
$status = 'TURUN';
}
kalau tidak {
$status = 'NAIK';
}
error_log("{$service->description()} mengubah status menjadi $status");
}
}
fungsi publik log_current_status(ServiceCheck$service)
{
error_log("{$layanan->deskripsi()}: $status");
}
}
Metode log_current_status() berarti jika suatu proses mengirimkan sinyal SIGUSR1, maka proses tersebut akan menyalin status lengkapnya ke log kesalahan PHP Anda.
Mesin menggunakan file konfigurasi sebagai berikut:
<config>
<penebang pohon>
<pencatat>
<id>log kesalahan</id>
<kelas>ErrorLog_ServiceLogger</kelas>
</pencatat>
<pencatat>
<id>email saya</id>
<kelas>EmailMe_ServiceLogger</kelas>
</pencatat>
</pencatat>
<layanan>
<layanan>
<kelas>HTTP_ServiceCheck</kelas>
<param>
<deskripsi>Pemeriksaan HTTP OmniTI</deskripsi>
<url> http://www.omniti.com </url>
<batas waktu>30</batas waktu>
<frekuensi>900</frekuensi>
</params>
<penebang pohon>
<pencatat>log kesalahan</pencatat>
<pencatat>email saya</pencatat>
</pencatat>
</layanan>
<layanan>
<kelas>HTTP_ServiceCheck</kelas>
<param>
<deskripsi>Pemeriksaan HTTP Halaman Beranda</deskripsi>
<url> http://www.schlossnagle.org/~george </url>
<batas waktu>30</batas waktu>
<frekuensi>3600</frekuensi>
</params>
<penebang pohon>
<pencatat>log kesalahan</pencatat>
</pencatat>
</layanan>
</layanan>
</config>
Saat meneruskan file XML ini, konstruktor ServiceCheckRunner membuat program logging untuk setiap log yang ditentukan. Kemudian, ini membuat instance objek ServiceCheck yang sesuai dengan setiap layanan yang ditentukan.
Perhatikan bahwa konstruktor menggunakan kelas Reflection_Class untuk mengimplementasikan pemeriksaan internal kelas layanan dan logging - sebelum Anda mencoba membuat instance mereka. Meskipun ini tidak diperlukan, ini dengan baik menunjukkan penggunaan API Refleksi baru di PHP 5. Selain kelas-kelas ini, Reflection API menyediakan kelas-kelas untuk mengimplementasikan inspeksi intrinsik pada hampir semua entitas internal (kelas, metode, atau fungsi) di PHP.
Untuk menggunakan mesin yang Anda buat, Anda masih memerlukan beberapa kode pembungkus. Pengawas harus mencegah Anda mencoba memulainya dua kali - Anda tidak perlu membuat dua pesan untuk setiap peristiwa. Tentu saja, monitor juga harus menerima beberapa opsi termasuk:
Deskripsi opsi
[-f] Lokasi untuk file konfigurasi mesin. Defaultnya adalah monitor.xml.
[-n] Ukuran kumpulan proses anak yang diizinkan oleh mesin.
[-d] Bendera yang menonaktifkan fungsionalitas daemon mesin ini. Ini berguna saat Anda menulis proses debug ServiceLogger yang mengeluarkan informasi ke stdout atau stderr.
Berikut adalah skrip pengawas terakhir yang mem-parsing opsi, memastikan eksklusivitas dan menjalankan pemeriksaan layanan:
require_once "Service.inc";
require_once "Konsol/Getopt.php";
$pilihan pendek = "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, "Gagal memperoleh kuncin");
KELUAR;
}
if(!$args['d']) {
jika(pcntl_fork()) {
KELUAR;
}
posix_setsid();
jika(pcntl_fork()) {
KELUAR;
}
}
fwrite($fp, getmypid());
fflush($fp);
$engine = new ServiceCheckRunner($args['f'], $args['n']);
$engine->loop();
Perhatikan bahwa contoh ini menggunakan fungsi getOptions() yang disesuaikan.
Setelah menulis file konfigurasi yang sesuai, Anda dapat memulai skrip sebagai berikut:
> ./monitor.php -f /etc/monitor.xml
Ini melindungi dan melanjutkan pemantauan hingga mesin dimatikan atau skrip dimatikan.
Skrip ini cukup rumit, namun masih ada beberapa area yang mudah diperbaiki, yang tersisa sebagai latihan bagi pembaca:
· Tambahkan handler SIGHUP yang menganalisis ulang file konfigurasi sehingga Anda dapat mengubah konfigurasi tanpa memulai server.
· Tulis ServiceLogger yang bisa login ke database untuk menyimpan data query.
· Tulis program front-end web untuk menyediakan GUI yang baik untuk seluruh sistem pemantauan.