Wydajniejsza kontrola dostępu do plików - virtual()

Problem

Bardzo często istnieje potrzeba zrealizowania zaawansowanej kontroli dostępu do plików, przy czym standardowe mechanizmy Apache nie wystarczają. Dzieje się tak, gdy decyzja o tym, czy plik należy udostępnić jest podejmowana przez aplikację na podstawie informacji niedostępnych dla serwera (np. stan punktowy konta użytkownika lub inne warunki wynikające z logiki aplikacji).

Szybkie rozwiązanie

Pierwszym rozwiązaniem, które przychodzi na myśl, jest użycie PHP i funkcji operujących na plikach - np. file_get_contents():


<?php
if(allowFileDownload($sFile)){
echo file_get_contents($sFile);
}else{
echo '403';
}
?>

W tym rozwiązaniu, za podjęcie decyzji, czy użytkownik ma prawo pobrać plik, odpowiada funkcja allowFileDownload(). Całość jest oczywista - jeżeli funkcja zwróci true, wysyłamy do przeglądarki zawartość pliku.

Większość z was zauważy pewnie, że powyższy przykład jest cokolwiek niekompletny. Należałoby uzupełnić go o wysyłanie nagłówków HTTP - przedewszystkim Content-type oraz Content-length. Wpis ten ma jednak traktować o czym innym, dlatego pominąłem wszystko co nie jest niezbędne. Kompletne podejście do udostępniania plików z wykorzystaniem PHP jest tematem na oddzielny wpis.

Szybsze rozwiązanie

PHP działające jako moduł Apache udostępnia funkcję virtual(). Funkcja ta wykonuje wewnętrzne żądanie Apache i przekazuje na wyjście jego wynik. Po zmodyfikowaniu powyższego przykładu, uzyskamy:


<?php
if(allowFileDownload($sFile)){
virutal($sFile);
}else{
echo '403';
}
?>

Co zyskujemy?

Przedewszystkim zyskujemy na wydajności, przy wysyłaniu większych plików. Aby porównać wydajność obu rozwiązań, wykorzystałem program ab.exe, o którym pisałem wcześniej. Podczas wykonywania testów, ustawiłem paramety programu na: ilość żądań - 100, równoległość żądań - 3. Testy przeprowadziłem dla 3 plików o różnej wielkości. Oto wyniki:

Rozmiar pliku file_get_contents() virtual()
650 b 88 ms 102 ms
1.15 mb 1241,99 532,97
1.80 mb 1789,07 ms 781,72 ms

Tabela zawiera średnie czasy odpowiedzi serwera. Jak widać, dla większych plików wydajność virtual() jest znacznie większa niż file_get_contents().

Zyskujemy także elastyczność i możliwość wykorzystywania wielu języków server-side w aplikacji. Jak? Należy pamiętać, że żądanie wykonane przez virtual() jest obsługiwane przez Apache. Jeżeli żądanie odnosi się do pliku CGI, to zostanie on wykonany. Dzięki temu możemy włączyć do strony, wynik działania np. skryptu Perla.

Co tracimy?

Przedewszystkim przenośność kodu. Funkcja virtual() jest dostępna tylko, gdy PHP działa jako moduł Apache lub jako moduł NSAPI w serwerach Netscape, iPlanet, SunONE. Odpadają więc wszystkie inne serwery oraz serwery gdzie PHP działa za pośrednictwem CGI.

Ponieważ pliki są udostępnione przez wywołanie żądania Apache, nie można przenieść ich poza drzewo dokumentów serwera. Dochodzi więc konieczność zabezpieczenia katalogu z chronionymi plikami przed dostępem z zewnątrz w taki sposób, aby jednak możnabyło wywołać żądanie z poziomu PHP. W manualu znalazłem rozwiązanie, polegające na odmowie dostępu do katalogu, z wyjątkiem tych żądań, które mają ustawioną zmienną PHP_ALLOW:


<directory katalog_z_plikami>
Order Allow,Deny
Allow from env=PHP_ALLOW
</directory>

W skrypcie PHP, przed wywołaniem funkcji virtual(), ustawiamy zmienną:

<?php
if(allowFileDownload($sFile)){
apache_setenv('PHP_ALLOW', '1');
virutal($sFile);
}else{
echo '403';
}
?>

O aspektach wykorzystania funkcji virtual() możnaby jeszcze conieco napisać, ale jestem pewien, że sięgniesz do manuala. Liczę na to, że wyniki wydajności, które przedstawiłem skłonią Cię do własnych testów. Jeżeli już je wykonasz, mam nadzieję, że się nimi podzielisz. Czekam zresztą na wszystkie uwagi, a nie tylko te, dotyczące wydajności.

5 odpowiedzi to “Wydajniejsza kontrola dostępu do plików - virtual()”

  1. splatch Says:

    Zgodzę się z Tobą odnośnie ograniczeń. W chwili obecnej używam PHP5 jako CGI i sama funkcja virtual nie zadziała. Opcja obsługi żądania przez Apache wydaje się atrakcyjna.. aczkolwiek, rzadko spotykana. Osobiście niewiele razy trafiłem na przypadek, gdy taka konieczność rzeczywiście zaistniała i była w 100% uzasadniona.

    Swoją drogą, myślę, że warto do całości porównać jeszcze wyniki readfile().

  2. Bigismall Says:

    Przyjemnie przeczytać coś o czym nikt wcześniej nie pisał … I nie dotyczy to tylko tego wpisu, ale większości na tym blogu.

  3. Jarosław Mężyk Says:

    @splatch: Rzeczywiście, virtual() jest dosyć rzadko wykorzystywana, niemniej warto o niej pamiętać.

    Co do trybu pracy PHP, to warto moim zdaniem zrezygnować z CGI na rzecz modułu Apache. Taki tryb pracy oferuje znacznie więcej niż funkcja virtual(). Przede wszystkim zyskujemy na wydajnośći. Możemy korzystać ze stałych połączeń z bazą (p. http://pl.php.net/manual/pl/features.persistent-connections.php) - to kolejny skok wydajności. Dalej - możemy korzystać z uwierzytelniania HTTP (http://pl.php.net/manual/pl/features.http-auth.php).

    @Bigismall: dzięki za uznanie. Staram się wybierać nie oklepane tematy. Tłumaczy to choć trochę, dlaczego wpisy na netcoffee.pl/pogodzinach pojawiają się tak rzadko.

  4. stormfly Says:

    Stałe połączenia to nie jest najlepszy pomysłem ponieważ w pewnym momencie nam się zablokuje liczba wolnych połączeń i serwer powie papa.

  5. Jarosław Mężyk Says:

    @stormfly: Stałe połączenia są bardzo dobrym pomysłem. Zawsze możesz zmienić w konfiguracji serwera bazy danych maksymalną ilość połączeń. Możesz także zmniejszyć czas, po którym nieaktywne połączenie jest zakańczane przez serwer bazy.

Nie zostawiaj tak tego! Dodaj komentarz!


statystyki