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.