Udostępnianie plików zalogowanym uzytkownikom

Witajcie,

Wpis ten poświęcony jest problemowi, który nieustannie pojawia się na forach (ostatnio znalazłem go na GoldenLine - jak udostępniać pliki tylko wyznaczonym użytkownikom (dopiero po zalogowaniu, użytkownicy należący do wybranej grupy, itd.). Chciałem wam przedstawić trzy rozwiązania - mam nadzieję, ze któreś z nich będzie dla was optymalne (niestety, rozwiązania idealnego chyba nie ma).

Rozwiązanie pierwsze: Dostęp kontrolowany przez aplikację

Zapytanie od użytkownika trafia do serwera aplikacji (serwer www uruchamia aplikację generującą dynamiczne strony WWW). Aplikacja sprawdza poziom dostępu użytkownika (sprawdza istnienie sesji, łączy się z bazą danych aby pobrać uprawnienia użytkownika, łączy się z bazą danych aby pobrać informacje o pliku) i przesyła plik do użytkownika lub zwraca komunikat o błędzie.

Jeśli zdecydujemy się na udostępnianie plików za pomocą tego rozwiązania, to musimy pamiętać o kilku ważnych sprawach:

  1. Należy uważać, aby nie zapisywać pliku w pamięci (pobieranie zawartości do pliku, a następnie wysyłanie zawartości tej zmiennej do przeglądarki), a wysyłać dane bezpośrednio do przeglądarki. Bardzo negatywnie wpływa to na wydajność, a dodatkowo przy większych plikach zwyczajnie zacznie nam brakować miejsca w pamięci (chyba, że wysyłamy do przeglądarki małe kawałki plików).
  2. Wyłączyć buforowanie danych wychodzących do przeglądarki. Powody jak powyżej.
  3. Należy pamiętać o obsłudze protokołu HTTP/1.1, a w szczególności nagłówków transmisji (typ pliku, rozmiar, wznawianie transmisji).

Polecam lekturę funkcji PHP readfile i komentarze do niej dołączone - niezależnie od ulubionego języka programowania.

Rozwiązanie to będzie działać zawsze, gwarantuje 100% kontrolę dostępu, ale jest najmniej wydajne.

Uzupełnienie:Okazuje się, że pierwszy sposób też może być wydajny, zapraszam do cz. 2 tego wpisu.

Rozwiązanie drugie: Losowa lokalizacja plików

Pliki do pobrania możemy umieścić w przypadkowej lokalizacji, np.:
http://example.org/1/23/45/67/890/ab/cd/e/f/ghijk/kon_rafal.avi

Przypadkowe odgadnięcie lokalizacji jest bardzo mało prawdopodobne, a uruchamianie aplikacji i kontakt z bazą danych nie jest wymagany. Wadą tego rozwiązania jest możliwość przekazania odnośnika innemu użytkownikowi.

Rozwiązanie trzecie: Wykorzystanie Mod Rewrite - RewriteMap

Rozwiązanie to pozwala nam swobodnie określić wymagany poziom wydajności i bezpieczeństwa, dla tego chciałbym je opisać bardziej szczegółowo.

Moduł mod_rewrite posiada dyrektywę (też macie skojarzenia z Unią Europejską [http://pl.wikipedia.org/wiki/Unia_Europejska]? :-) ) RewriteMap, która pozwala zdefiniować mapę przekształceń, czyli a->b, c->d itd. Do dyspozycji mamy następujące typy map:

  • Standard Plain Text - naprostsza mapa 1:1 zapisana w pliku tekstowym. np.:
    Robert.Boguszewski      talen
    Jan.Kowalski            jkowalski
    
  • Randomized Plain Text - podobnie jak w w przypadku Standard Plain Text, ale możemy podać kilka alternatyw, np.:
    www    www1|www2|www3
    
  • Hash File - podobnie jak powyżej, ale plik nie jest zapisany w postaci czystego tekstu, ale w formacie NDBM - przetwarzanie większych map przekształceń będzie szybsze, ponieważ klucze są indeksowane
  • Internal Function - wybieramy funkcję przekształcająca string spośród: toupper, tolower, escape, unescape
  • External Rewriting Program - możemy zdefiniować własną funkcję która będzie przekształcała nam adres w jednej postaci w inny adres.

Do rozwiązania problemu ograniczania dostępu dla wybranych użytkowników wykorzystamy właśnie External Rewriting Program, a osoby bardziej zainteresowane tematem zapraszam do oficjalnej dokumentacji.

Przykładowe rozwiązanie

1. Konfiguracja wirtualnego hosta:
Niestety, ale mapy musimy definiować na poziomie konfiguracji serwera - nie ma możliwości dodania mapy na poziomie .htaccess. Do konfiguracji wirualnego hosta dodajemy:

2. Konfiguracja .htaccess:

Założyłem, że wirtualny host odpowiada wyłącznie za udostępnianie plików. Ponieważ w skrypcie mamy bardzo ograniczony dostęp do zmiennych (praktycznie nie ma w nich nic użytecznego), przekazujemy w formacie adresIp_UriPliku wszystkie potrzebne informacje których zamierzam użyć. Przykładowe dane otrzymane przez skrypt na STDIN będą wyglądały następująco:
127.0.0.1_/1/2/3/4/kon_rafal.avi

3. Skrypt dokonujący odkodowania adresu:
Skrypt poniżej działa w oparciu o bardzo prosty algorytm "kodujący": każdy plik znajduje się w 4 poziomach katalogów, a kodowanie polega na dodaniu do numerycznej nazwy katalogu adresu IP użytkownika, np.:
IP użytkownika: 127.0.0.1
Lokalizacja pliku: to /1/2/3/4/kon_rafal.avi
Url dla użytkownika to: /128/2/3/5/kon_rafal.avi

Skrypt jest uruchamiany podczas uruchamiania Apache i komunikuje się z nim za pomocą STDIN i STDOUT. Przez STDIN przesyłany jest ciąg znaków zakończony znakiem nowej linii "\n", a jako odpowiedź oczekiwany jest również taki ciąg znaków.

Nie wiem czy poniższy skrypt wymaga komentarza, ale jeśli będzie potrzebny to chętnie dopiszę.
A teraz wyjaśnienia:

Uwagi do przygotowywania skryptów
  1. Program uruchamiany jest tylko raz i działa cały czas w tle.
  2. Należy pamiętać o wyłączeniu buforowania wyjścia (u mnie: ob_implicit_flush(true) i flush());
  3. KISS (Keep It Simple Stupid) - Skrypt powinien być maksymalnie prosty i przygotowany możliwie w języku niższego rzędu. Przerost formy nad treścią najprawdopodobniej skończy się przeciążeniem serwera. Prosty skrypt, to szybsze działanie i mniejsza szansa popełnienia błędu. Już samo wykorzystanie w powyższym przykładzie PHP wywołuje pewien niesmak, więc bardzo proszę, aby powstrzymać się przed zaprzęganiem Zend Framework[http://framework.zend.com] i bazy danych do obsługi takich skryptów.
  4. [warn] mod_rewrite: Running external rewrite maps without defining a RewriteLock is DANGEROUS! - taki komunikat zobaczycie w logach Apache jeśli uruchomićie mapowanie przez zewnętrzny skrypt bez definiowania RewriteLock, który służy do synchronizacji pracy skryptu.
Podsumowanie

RewriteMap jest bardzo fajnym rozwiązaniem, które mądrze zastosowane może okazać się naprawdę przydatne. Nie nie przeprowadziłem żadnych testów wydajnościowych, ale mam podstawy przypuszczać, że taki sposób udostępniania zasobów użytkownikom zalogowanym będzie lepszy niż uruchamianie aplikacji. RewriteMap wymaga dostępu do konfiguracji wirtualnego hosta, co niestety ogranicza zakres wykorzystania praktycznie do serwerów dedykowanych.

Komentarze

  1. hth
    21 września 2009 | #

    Yyyyy? A po co jak jest .htaccess?

  2. 21 września 2009 | #

    Myślę, że nie będzie obciążać serwera w żaden zauważalny sposób. Można by go nawet zrobić na Visual Basic :) W końcu, jak sam napisałeś, odpala się tylko raz i działa sobie w tle.

  3. 21 września 2009 | #

    Rozwiązanie pierwsze wcale nie musi być mało wydajne, wystarczy skorzystać z nagłówka HTTP X-ACCEL-REDIRECT (Nginx) lub X-SENDFILE (Apache). Działa to tak, że w aplikacji można sprawdzić uprawnienia do pliku, a następnie zrobić przekierowanie np. X-SENDFILE do konkretnego pliku. W ten sposób omija się problemy z pamięcią przy wczytywanie całego pliku przez skrypt, albo babranie się z przesyłaniem porcjami. I jest wydajne.

  4. 22 września 2009 | #

    Bardzo dobry post, ale też mi się wydaje, że niektóre rozwiązania wcale nie są tak wydajne

  5. 24 września 2009 | #

    wydajne bądź nie, grunt że działają.

  6. 01 października 2009 | #

    @Jaro: Dzięki ! Nie znałem tego nagłówka o jest on naprawdę fajnym rozwiązaniem. Napisałem już cz2 tego wpisu: http://talen.jogger.pl/2009/10/01/udostepnianie-plikow-zalogowanym-cz-2-x-sendfile-prosciej-ju/

    @hth: No za pomocą zwykłego .htaccess nie zrobisz tego o czym napisałem...

  7. Bolo
    24 października 2010 | #

    Uwaga do tekstu. Jest zdanie: "Nie wiem czy poniższy skrypt...", powinno być chyba powyższy skrypt :)
    Pozdrawiam

    P.S. Nie ma informacji czy wysłało komentarz czy nie :/ Nie wiem czy ze 4 razy przez to nie dodałem tego komentarza :(

Napisz komentarz