Logi serwera w PHP

Adam Boduch

W systemach typu UNIX oraz ogólnie na serwerach ważną rolę odgrywają tzw. logi, czyli dziennik systemowy. Taki "dziennik" zawiera dane dotyczące przebiegu działań użytkownika np. - na serwerze. W przypadku serwera Apache takie logi dostarczają informację o użytkowniku oglądającym Twoją stronę (wchodzącym na serwer HTTP) jak np. adres IP, przeglądarkę, czas wizyty itp. Podstawowym pytaniem jest: do czego tak właściwie służą logi?

Podstawową kwestią jest kwestia bezpieczeństwa. W razie próby włamania na serwer, administrator ma możliwość zlokalizowania adresu IP z którego korzystał włamywacz i jeżeli jest to możliwe (czyt. jeżeli włamywacz takie informacje po sobie zostawił) - zgłosić przestępstwo do odpowiednich organów. Drugą kwestią są statystyki które dostarczają informacji na temat ilości wizyt, wywołań itp. Takie statystyki są właśnie sporządzane na podstawie logów serwera. Pomijam tu oczywiście takie darmowe systemy statystyk jak np. STAT4U, które jednak nie są tak wiarygodne jak statystyki na podstawie logów.

Logi to w rzeczywistości plik tekstowy (najczęściej z rozszerzeniem *.log), którego każda linia stanowi osobne wywołanie. Istnieją także logi, w których zapisane są błędy spowodowane przez aplikację (najczęściej nazwa takiego pliku to error.log). Dzięki temu np. autorzy owej aplikacji oraz administrator mają większe szanse na zlokalizowanie przyczyny.

Budowanie systemu logów

Większość <wiki href="provider">providerów</wiki> nie udostępnia logów do wglądu dla użytkownika (klienta). Jedną przyczyną jest to, że takie pliki tekstowe zajmują wiele miejsca (szczególnie przy popularniejszych serwisach) i najczęściej są kasowane po pewnym okresie czasu (np. tygodniu). Tematem tego artykułu jest zbudowanie własnego systemu logowania w PHP. Oczywiście nie będzie on tak wiarygodny i dokładny jak ten z Apache'a lecz może być jako takim "zabezpieczeniem" w razie jakiś ataków, próby spamu (np. na forach dyskusyjnych) itp. Co będzie Ci potrzebne? Zakładam, że Twój serwis WWW działa w oparciu o technologię PHP. Podczas budowania serwisów dynamicznych najlepiej mieć jakiś centralny, główny plik, w którym znajdują się najczęściej używane funkcje - np. plik functions.php. Może on np. wyglądać tak:

<?php

/* przykładowo - dołączenie do naszego pliku "głównego" innych modułów */
include 'config.inc';
include 'include/plik.php';

?>

Teraz każdy inny plik PHP w serwisie powinien dołączać, właśnie - plik functions.php. Przykładowo jeżeli posiadasz skrypt do rejestracji użytkownika (nazwijmy go newuser.php) to powinien on wyglądać tak:

<?php

include 'functions.php'; // dołączenie "głównego modułu"

/* dalsza część skryptu */
?>

Czemu o tym mówię? Otóż nasz skrypt prowadzący dziennik musi być wywoływany za każdym razem, gdy użytkownik wejdzie na stronę. Umieszczając kod takiego skryptu w pliku functions.php mamy pewność, że będzie on wykonywany za każdym razem.

Założenia

Należy sobie zadać pytanie: jakie informację od użytkownika ma zbierać nasz skrypt? Powiedzmy, że ma to być:

  • IP użytkownika
  • czas wywołania strony
  • IP wewnętrzny użytkownika
  • host użytkownika
  • URL jaki użytkownik odwiedził
  • nazwę przeglądarki z jakiej korzystał użytkownik
  • ID użytkownika 1

Wszystkie dane będą gromadzone w pliku acccess.log, który zostanie utworzony na serwerze; dane będą zapisywane w takiej postaci:

80.51.77.0 ( 10.1.1.153 ), ID: 0 piotrkow1.zzz.pl - [10-11-2003 18:31:42] /java/index.html

Pierwszy człon zawiera IP zew.; w nawiasie znajduje się IP wew. dalej ID użytkownika, jego host, data wystąpienia zdarzenia oraz plik, jaki próbował odczytać - w tym wypadku /java/index.html. Oczywiście to tylko przykład - celowo pomiąłem tutaj informację, np. na temat przeglądarki.

Kod źródłowy skryptu

<?php
/****************************************************************************
*                                log.php
*                         -------------------------
*                      rozpoczety    : 28.07.2003 r.
*                      wersja        : $Revision: 1.1 $
*                      zmiana        : $Date: 2003/07/28 11:12:09 $
*
****************************************************************************/

  $log_dir = '/sciezka_na_serwerze/do_naszego_serwisu';

  // proba otwarcie pliku access.log lub - w przypadku gdy zrodlowy nie istnieje - utworzenia go 
  $file = @fopen($log_dir . '/access.log', 'a');
  @flock($file, 2); // blokowanie pliku - wylacznosc na zapis i odczyt
  
  $string = '%remote_ip% ( %forwarded_for% ), ID: %user% %host% - [%time%] %url%'; // z konfiguracji odczytanie formatu zapisu logu
  
  $string = str_replace('%remote_ip%', getenv('REMOTE_ADDR'), $string);
  $string = str_replace('%host%', gethostbyaddr(getenv('REMOTE_ADDR')), $string);
  $string = str_replace('%forwarded_for%', getenv('HTTP_X_FORWARDED_FOR'), $string);
  $string = str_replace('%time%', date('d-m-Y H:i:s'), $string);
  $string = str_replace('%url%', getenv('REQUEST_URI'), $string);
  $string = str_replace('%user%', (isset($cookie) ? $cookie['id'] : 0), $string); // te linie możesz usunąć - obowiązuje jedynie w projekcie Coyote
  $string = str_replace('%user_agent%', getenv('HTTP_USER_AGENT'), $string);

/* poczwszy od wersji 4.0.1 funkcja str_replace() moze przybierac wartosci ktore sa tablica */
  
  @fwrite($file, $string . "n"); // zapisanie linii
  @flock($file, 3); // odblokowanie pliku
  @fclose($file);
  
// $Id: log.php,v 1.1 2003/07/28 11:12:09 Adam Exp $

?>

Powyższy fragment kodu pochodzi z projektu OpenSource o nazwie Coyote który obsługuje serwis 4programmers.net
Ważnym fragmentem tego skryptu jest zmienna <font face="Courier">$string</span> do której przypisywany jest tekst - tzw. format logu. Miejsce rozpoczynające się
i kończące znakiem % zostanie zastąpione odpowiednimi wartościami (danymi na temat użytkownika) za pomocą funkcji str_replace. Oczywiście format
logu (zmienna $string) możesz ustalać sam wedle własnego uznania. Jeżeli potrzebujesz jedynie IP użytkownika oraz nazwy przeglądarki z jakiej korzystał,
możesz napisać tak:

$string = '%remote_ip% %user_agent%';

Nie zapomnij do zmiennej $log_dir przypisać ścieżki, gdzie ma zostać zapisany plik access.log. To praktycznie wszystko. Cała budowa skryptu jest bardzo prosta.
Za każdym razem gdy skrypt jest wywoływany otwierany zosaje plik access.log i zostaje dopisana do niego kolejna linia. Znak <font face="Courier">@</span> przed instrukcjami PHP
gwarantuje to, iż nawet w razie wystąpienia błędu, użytkownik tego nie odczytuje.

Dołączenie skryptu

Powyżej przedstawiłem całą zawartość skryptu, skopiuj ten tekst i zapisz do pliku - np. log.php, a następnie umieść owy plik na serwerze. Następnie otwórz
plik functions.php i dopisz do niego linię:

include 'log.php';

Dzięki temu do skryptu functions.php dodana zostanie zawartość pliku log.php, który dokona zapisu w dzienniku.

Kwestie bezpieczeństwa

Jeżeli nasz plik - access.log jest zapisany w katalogu do którego ma dostęp serwer, niesie to za sobą pewne niebezpieczeństwo, iż zwykły użytkownik znając
lokalizację pliku access.log będzie mógł zobaczyć nasz dziennik, zobaczyć wszystkie adresy IP itp. Dlatego możemy w sprytny sposób zablokować możliwość
podglądu pliku access.log z poziomu przeglądarki (do pliku wgląd będziesz miał jedynie Ty poprzez program FTP lub <wiki href="shell">shella</wiki>).
Wystarczy, iż Twój Apache obsługuje pliki .htaccess (kropka na początku). W takim wypadku możesz na serwerze, w katalogu, gdzie znajduje się
plik access.log umieścić plik .htaccess z następującą zawartością:

<FilesMatch "access.log">
deny from all
allow from ...
</FilesMatch>

404

404 oznacza błąd jaki zostaje zwracany przez serwer, w przypadku, gdy plik, który próbuje wczytać użytkownik - nie istnieje. Na wielu serwerach istnieje
możliwość wyświetlania w takim wypadku własnego komunikatu. Wystarczy możliwość obsłużenia plików .htaccess, który powinien mieć taką zawartość:

ErrorDocument 404 /404.php

Dzięki temu w razie wystąpienia błędu 404, zostanie wczytany plik 404.php. Dlaczego o tym piszę? Otóż dzięki temu w naszych logach można
umieścić także informację o niedzaiałających plikach. Wystarczy, że plik 404.php będzie miał taką zawartość:

<?php
// plik 404.php
include 'functions.php';
echo 'Taki plik nie istnieje';
?>

Dzięki temu oprócz wyświetlenia standardowego komunikatu, w logach zostanie odnotowana próba załadowania pliku, który nie istnieje.

Czyszczenie logu

Wiadomo - z dnia na dzień plik logu zajmuje coraz więcej miejsca na serwerze. Warto więc czyścić go co jakiś czas (ew. kopię zapisywać na np. miesiąc na innych partycji). Aby zbytnio się nie przemęczać, warto może pomyśleć o programowanie cron, który w środowisku UNIX służy do okresowego uruchamiania. Hmm... może niezbyt wyraźnie się wyraziłem. Dzięki tej aplikacji można zaprogramować czasowe uruchamianie swojego np. skryptu - raz w tygodniu. Program cron "budzi się" co 1 min. i czyta pliki konfiguracji. Tak więc możesz zaplanować uruchamianie jakiś poleceń co minutę, co 5 min., raz w tygodniu, raz w miesiącu, w określone dni tygodnia, albo nawet raz w roku. Uwaga! To czy posiadasz dostęp do crona, zależy od Twojego administratora. W każdym razie wymagane będzie posiadanie na serwerze konta shell.

W naszym przykładzie posłużymy się skryptem napisanym w <wiki href="Perl">Perlu</wiki>, który będzie czyścił zawartość naszego dziennika. Taki skrypt należy umieścić na serwerze - najlepiej w miejscu do którego nie ma dostęp Apache. Przykładowo u mnie na serwerze pliki, do których ma dostęp Apache, nalezy umieszczać w katalogu 'public_html'. Tak więc taki skrypcik można umieścić katalog wyżej w stosunku do 'public_html'. Dzięki temu użytkownik nie mający dostęp do Twojego konta shell nie będzie w stanie uruchomic tego skryptu.

Skrypt powinien wyglądać mniej więcej tak:

#!/usr/bin/perl

  $url = 'public_html';


  open (FILE_HANDLE, '&gt;' . $url . '/access.log');
  close (FILE_HANDLE);

# $Id: empty.pl,v 1.1 2003/07/29 16:23:54 Adam Exp $

Skrypt ma prostą budowę - jego zadaniem jest otwieranie pliku access.log z parametrem '>', który powoduje czyszczenie pliku.

Nie zapomnij w zmiennej $url przypisać prawidłowej ścieżki (nazwy katalogu), w którym znajduje się plik access.log.

Teraz należy ustawić odpowiednie parametry crona. Zaloguj się na swoje konto i w konsoli wpisz polecenie:

crontab -e

Jeżeli uruchomi się domyślny edytor to znaczy, że wszystko jest ok. W edytorze tym wpisz taką linię:

01 0 * * 0 (cd /home/sciezka_do_serwera; perl empty.pl)

Taka instrukcja spowoduje przejście do katalogu /home/sciezka_do_serwera oraz uruchomienie skryptu Perla - empty.pl, który wyczyści plik access.log. Cała ta operacja zostanie wykonana o 1.00 rano w niedziele.

Uwaga! Pamiętaj aby skryptowi Perla nadać prawa wykonywania - 0775.

Więcej informacji na temat crontab'a możesz znaleźć w pomocy systemowej - polecenie man crontab.
Dodatkowe linki:


1 w tym przykładzie ID użytkownika odnosi się do projektu Coyote i do serwisu 4programmers.net, gdzie ID zarejestrowanego użytkownika znajduje się w cookie.

8 komentarzy

php da się uruchomić z linii poleceń. noo prawie... :)

C:\php> php

<? echo "Ręcznie uruchomiony skrypt PHP"; ?>

^Z
Ręcznie uruchomiony skrypt PHP
C:\php>

ten "wydruk" był pisany ręcznie, nie był kopiowany z linii poleceń ale tak to mniej więcej działa. kod php trzeba wpisać ręcznie lub użyć do tego programu jakiegoś który odpala php i wpisuje dane sam, a później wstawia znak ^Z (CTRL+Z). Wszystko po tym znaku to wyjście php czyli rezultat. oczywiscie usunięcie zawartości pliku chyba żadnego tekstu zwracać nie będzie...

można też tak:

C:\php> php C:\WWW\clear.php

co wykona skrypt "clear.php" i zwróci jego wynik tak jak poprzednio.

EDIT: Tak jest w php >= 4.13 bo na takich tylko pracowałem
(innych nie testowałem)

Hmm... tak, ale ten skrypt byl uzywany na potrzeby serwisu, a chodzilo o to, zeby cron mogl uruchamiac systematycznie skrypt - a PHP nie dalo sie uruchamiac z linii polecen...

jedna sugestia - zamiast wywoływać kilka (kilkanaście) razy str_replace można by wywołać raz z tablicami jako dwa pierwsze parametry :] myślę że było by szybciej a na pewno czytelniej :) [pozrawiam, rogrog

Zamiast pisać skrypt w perlu można po prostu zrobić tak:

$s = @filesize('access.log');
  if (!@file_exists('access.log') | $s >= 1048576) //jeden megabajt
      $file = @fopen('access.log', 'w+');
  else
      $file = @fopen('access.log', 'a+');

Ale to szczegół. Poza tym bardzo fajny art ;)

i tak przy okazji kwestii bezpieczenstwa.. PHP ma to do siebie ze jest wykonywany przez Apache, co za tym idzie, ma usera i grupe taka sama jak Apache, a wiec wszystkie pliki ktore tworzy PHP maja grupe taka jak Apache. Więc jeśli tworzymy plik z prawami u=rwx,go=r to tylko apache ma do tego pliku prawa, a wiec nie mozemy go jako wlasciciel konta kopiowac, usuwac, edytowac, TYLKO CZYTAĆ!!! ponad to skrypt uruchamiany w cronie bedzie uruchamiany tak jakby uruchamial go uzytkownik, wiec nie bedzie dzialac...

To jest tylko pikuś :) teraz najgorsze, wszyscy uzytkownicy tworzacy skrypty moga miec do tego pliku dostep (edytowac, kasowac, zmieniac, podgladac plik), juz tlumacze dlaczego.. :) taki "haker" wystarczy ze wie jaka jest wewnetrzna sciezka do tego pliku, pozniej tworzy skrypt php ktory otworzy ten plik i zrobi z nim co chce... (np. wyswietli) pozniej kopiuje sobie do katalogu w ktorym sa strony, wywoluje z przegladarki ten skrypt i juz.. wszystko mu pokaze (jesli skrypt jest zaprogramowany TYLKO do czytania), ale moze zrobic tak naprawde z tym plikiem WSZYSTKO.

jedynym wyjatkiem sa php uruchamiane jako CGI, wtedy wszystkie pliki tworzone przez php maja prawa wlasciciela skryptu (pliku PHP). Ale zapewne wszystkie serwery nie maja skryptów jako CGI, gdyż to bardziej spowalnia i nadwyręża serwer...

Reasumujac, z plikiem logow znajac wew. sciezke, mozna zrobic wszystko! Wiec smiem podważać bezpieczenstwo tego skrypty :) No chyba ze sie myle.. jesli tak to niech ktos mnie poprawi :)

hm.. :) moze sie czepiam, ale file_exists jest zbedne, gdyż według php.net parametr a+ i a podany funkcji fopen() powoduje dopisanie, lub jesli nie ma pliku stworzenie go...

hyhy Pedros nie zdziwił bym się, jakby po wyjściu coyotka 1.0 pojawiła sie książeczka Adama :D

O Adam wzial sie powaznie za arty o php a moze ksiazka o php? :))