Tvorba internetových aplikací

Jakub Klinkovský

:: České vysoké učení technické v Praze
:: Fakulta jaderná a fyzikálně inženýrská
:: Katedra softwarového inženýrství

Akademický rok 2023-2024

Download a upload souborů

Jak funguje download

Tradiční způsob stahování souborů pomocí protokolu HTTP:

  1. URL určí umístění daného souboru na webu
  2. Klient vyšle požadavek metodou GET
  3. Server odešle odpověď s požadovanými daty
  4. Klient zpracuje odpověď (zobrazení multimediálního souboru v prohlížeči nebo uložení souboru na disk)

Tento postup je dostatečný pro malé soubory, které jsou typicky zobrazovány přímo v prohlížeči. Požadavky jsou vysílány a zpracovávány asynchronně, může jich být velké množství.

Efektivnější download

Pro zvýšení efektivity vznikla řada rozšíření protokolu HTTP:

  • pokračování přerušeného přenosu (hlavičky Accept-Range, Range, Content-Range a stavový kód 206 Partial Content)
  • kódování souboru po částech – umožňuje tzv. streaming (v protokolu HTTP/1.1 pomocí hlavičky Transfer-Encoding: chunked, HTTP/2 má vlastní mechanismy)
  • streamování dat s adaptivní změnou datového toku (vice na Wikipedii)

Pro specifické aplikace existují pokročilé protokoly (např. WebRTC pro real-time přenos obrazu a zvuku mezi klienty).

Jak funguje upload

  1. Klient má k dispozici dokument s formulářem, který obsahuje prvek (prvky) určené pro upload souboru (viz dále)
  2. Klient zakóduje data souboru do těla HTTP požadavku typu POST nebo PUT, který odešle na server
  3. Server zpracuje přijatá data (např. provede validaci, uloží do databáze nebo na disk)
  4. Server odešle odpověď obsahující výsledek zpracování uploadovaných dat

Samotný přenos dat protokolem HTTP probíhá analogicky jako v případě downloadu.

Upload a bezpečnost

Aplikace umožňující upload souborů je potenciálně nebezpečná:

  1. Maximální velikost uploadovaných souborů by měla být omezená.
  2. S uploadovanými soubory by se vždy mělo zacházet jako se statickými daty.
    Nikdy nespouštějte skripty uploadované z neověřeného zdroje!
  3. Typ dat by se měl vždy validovat (např. jestli PNG obrázek obsahuje opravdu obrazová data a ne třeba HTML).

Pro validaci dat neexistuje žádné univerzální řešení. Různé způsoby pro omezení bezpečnostních důsledků si vysvětlíme a ukážeme později.

Formuláře pro upload souborů

Pro upload souborů je nutné specifikovat typ kódování dat pomocí form-data. K tomu slouží atribut enctype značky <form>:

<form enctype="multipart/form-data" action="/upload/" method="post">
    ...
</form>

Prvky formuláře pro upload souborů

Existuje značka <input type="file">. Příklad:

<label for="avatar">Choose a profile picture:</label>

<input type="file"
       id="avatar" name="avatar"
       accept="image/png, image/jpeg">

Atributy:

  • accept – seznam akceptovaných typů dat
  • capture – umožňuje vytvořit nový soubor (obrázek, video, zvuk) pomocí kamery
    (v současnosti podporují jen prohlížeče mobilních zařízení)
  • multiple – umožňuje vybrat více souborů současně

Formuláře pro upload v Django

Prvky objektových formulářů: FileField a ImageField (třída odvozená od FileField).

Poznámka: pro modely také existují FileField a ImageField (viz dále).

Příklad:

from django import forms

class UploadFileForm(forms.Form):
    file = forms.FileField()

Pozor: atribut max_length pro FileField omezuje délku názvu souboru, ne obsahu!

Atribut allow_empty_file určuje, jestli formulář při validaci dat akceptuje soubory s prázdným obsahem.

Vytvoření prázdného formuláře

Pro zobrazení prázdného formuláře stačí jednoduchá funkce ve views.py:

def index(request):
    variables = {"form": forms.UploadFileForm()}
    return render(request, "pokus/index.html", variables)

Při formátování formuláře v šabloně je nutné uvést atribut enctype pro značku <form>, např.:

<form enctype="multipart/form-data" action="{% url 'upload' %}" method="post">
    {% csrf_token %}
    {{ form.as_div }}
    <input type="submit" value="Send">
</form>

Zpracování formuláře s uploadem – kontrola HTTP metody

V souboru views.py vytvoříme funkci, která bude zpracovávat pouze upload:

def upload(request):
    # upload must be submitted via the POST method
    if request.method != 'POST':
        # return HTTP 405: Method Not Allowed
        return HttpResponseNotAllowed(["POST"])

    ...

Objekt HttpResponseNotAllowed představuje odpověď se stavovým kódem HTTP 405.

Zpracování formuláře s uploadem – validace

Dále vytvoříme instanci formuláře navázanou na slovníky s parametry požadavku (request.POST) a uploadovanými soubory (request.FILES):

    ...

    # create a form with data from the request
    form = forms.UploadFileForm(request.POST, request.FILES)

    # check if the form is valid
    if not form.is_valid():
        # render the template showing errors in the form
        variables = {"form": form}
        return render(request, "pokus/index.html", variables)

    ...

Zpracování formuláře s uploadem – uložení souboru

Po validaci formuláře zpracujeme samotná data uploadovaných souborů. V tomto příkladu jen soubor request.FILES["file"], kde "file" odpovídá atributu file v daném formuláři. Objekty ve slovníku FILES mají typ UploadedFile.

    ...

    # do something with the uploaded file
    save_uploaded_file(request.FILES["file"])

    # Always return an HttpResponseRedirect after successfully dealing
    # with POST data. This prevents data from being posted twice if the
    # user hits the <Back> or <Reload> button.
    return HttpResponseRedirect(reverse("index"))

Poznámka: v tomto příkladu nepoužívám ModelForm – soubor je tedy nutné zpracovat ručně. ModelForm by se postaral o uložení všech dat, včetně souborů.

Pohled pod pokličku – Django upload handlers

Jakmile Django začne zpracovávat požadavek (objekt request), data uploadovaných souborů ještě nemusí být k dispozici. V průběhu zpracování jsou různé atributy požadavku, včetně uploadovaných souborů, vytvářeny stylem on demand.

Přístup ke slovníku request.FILES vyvolá použití tzv. file handler objektů, které se postarají o samotný upload na dočasné místo a vytvoření objektu UploadedFile. Defaultní nastavení v settings.py je:

FILE_UPLOAD_HANDLERS = [
    'django.core.files.uploadhandler.MemoryFileUploadHandler',
    'django.core.files.uploadhandler.TemporaryFileUploadHandler',
]

Do velikosti 2.5 MB se použije MemoryFile, jinak TemporaryFile na disku.

Lze implementovat vlastní file handler (např. ProgressBarUploadHandler pro AJAX).

Vsuvka – práce se soubory a adresáři v Pythonu

Proměnná __file__ – string s cestou ke zdrojovému souboru aktuálního modulu

Funkce open() – otevření souboru na disku pro čtení nebo zápis

Moduly pro práci se soubory:

  • pathlib (nebo starší os.path) – třída Path pro reprezentaci cest v souborovém systému a příslušné operace
  • shutil – vysokoúrovňové operace se soubory a stromovými strukturami na disku (kopírování, přesouvání, mazání, archivování)
  • tempfile – vytváření dočasných souborů a adresářů
  • filecmp – porovnávání souborů a adresářů

A další nízkoúrovňové moduly, viz oficiální přehled.

Viz také ZPRO 2023: cvičení 19 a cvičení 20.

Dokončení zpracování uploadu

Funkci save_uploaded_file můžeme implementovat takto:

def save_uploaded_file(uploaded_file):
    # choose upload directory
    upload_dir = Path(__file__).parent / "uploads"

    # create the directory
    upload_dir.mkdir(parents=True, exist_ok=True)

    # set upload file path
    upload_path = upload_dir / uploaded_file.name

    # save the uploaded data
    with open(upload_path, "wb+") as f:
        for chunk in uploaded_file.chunks():
            f.write(chunk)

Uploadování více souborů

Pro umožnění uploadu více souborů v jednom fieldu jsou potřeba tyto změny:

  1. Použít atribut multiple pro značku <input type="file">. V objektovém světě Django se to provede následovně:

    class UploadFileForm(forms.Form):
        file = forms.FileField(widget=forms.ClearableFileInput(
                                                 attrs={"multiple": True}))
    
  2. Zpracovat seznam souborů ve funkci ve views.py:

    def upload(request):
        ...
        # do something with the uploaded files
        for file in request.FILES.getlist("file"):
            save_uploaded_file(file)
        ...
    

Omezení typu souborů

  1. Pro značku <input type="file"> je vhodné použít atribut accept:

    class UploadFileForm(forms.Form):
        file = forms.FileField(
            widget=forms.ClearableFileInput(attrs={"accept": "application/pdf"})
        )
    
  2. Na straně aplikace by se měla provést validace, minimálně ověřit content_type pro UploadedFile ve funkci save_uploaded_file:

        ...
        if uploaded_file.content_type != "application/pdf":
            raise ValidationError("only PDF files are allowed")
        ...
    

    Obsah souboru by se měl také ověřit!

Objektové zpracování uploadu a spolupráce s databází

Django umožňuje zpracovat uploadované soubory ve spolupráci s modely:

  • třída FileField pro definici atributů modelu
  • propojení uploadovaných souborů s objektově-relačním mapováním
  • data se uloží na disku a v databázi se uloží jen cesta k souboru

Příklad: definice v models.py:

from django.db import models

class MyModel(models.Model):
    # file will be uploaded to MEDIA_ROOT/uploads
    upload = models.FileField(upload_to="uploads/")

Alternativně lze použít tokeny pro formátování funkcí strftime(), např.
upload_to="uploads/%Y/%m/%d/" uloží soubory do adresáře
MEDIA_ROOT/uploads/2024/04/19

Volby pro umístění uploadovaných souborů

V souboru settings.py lze definovat:

  • MEDIA_ROOT – umístění souborů na disku
  • MEDIA_URL – část URL pro přístup k souborům ze strany klienta

Např.:

MEDIA_ROOT = BASE_DIR / "media"
MEDIA_URL = "media/"

V urls.py je potřeba nastavit mapování (funguje jen v debug režimu):

from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
    ...
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

Zpracování uploadu pomocí modelu s atributem FileField

Funkce upload v souboru views.py bude vypadat stejně jako v předchozím příkladu, až na to, že místo použití funkce save_uploaded_file:

    # do something with the uploaded file
    save_uploaded_file(request.FILES["file"])

vytvoříme instanci modelu ("file" odpovídá atributu v definici formuláře a upload= odpovídá atributu v definici modelu):

    # save the uploaded file to a model
    instance = models.MyModel(upload=request.FILES["file"])
    instance.save()

Metoda save() se postará o správné uložení dat, tj. uložení souboru na disk a cesty k souboru do databáze. Pokud použijete ModelForm, stačí uložit jen formulář 😄

Práce se soubory uloženými pomocí modelu

K souborům uloženým tímto způsobem se přistupuje pomocí třídy FieldFile. Např.:

f = MyModel.objects.first()     # instance třídy MyModel
f.upload                        # instance třídy FieldFile

Důležité atributy:

  • name – relativní cesta vzhledem k úložišti
  • path – cesta k souboru v souborovém systému
  • url – relativní URL pro přístup k danému souboru

Poznámka: Všimněte si, že v definici modelu se použila třída FileField.

Příklad: zobrazení seznamu uploadovaných souborů

Ve funkci index v souboru views.py přidáme dotaz na záznamy v databázi:

def index(request):
    ...

    variables["uploads"] = models.MyModel.objects.all()
    return render(request, "pokus/index.html", variables)

V šabloně naformátujeme seznam souborů s odkazy:

<ul>
{% for file in uploads %}
    <li><a href="{{ file.upload.url }}">{{ file.upload.name }}</a></li>
{% endfor %}
</ul>

Správa souborů a úložiště

Defaultní úložiště pro soubory v Django je lokální souborový systém – viz MEDIA_ROOT.
Kromě toho je možné implementovat vlastní úložiště – např. pro vzdálené systémy.
Projekt django-storages poskytuje rozhraní pro Amazon S3, Dropbox, atd.

Více o správě souborů v Django najdete v sekci Managing files.

Použití třídy ImageField

Místo třídy FileField lze v definici modelu nebo formuláře použít třídu ImageField, která poskytuje dodatečné funkce pro zpracování obrázků:

  • pro značku <input> se automaticky použije atribut accept="image/*"
  • v aplikaci se provede validace koncovky souboru (např. .jpg, .png, atd.)
  • validace obrazových dat v souboru (ale není to 100%...)
  • využívá knihovnu Pillow, kterou je nutné nainstalovat:
    pip install Pillow
    
  • objekt UploadedFile má dodatečný atribut image, který odpovídá instanci PIL.Image použité pro validaci – soubor se ale po validaci uzavře, takže jsou k dispozici jen metadata (např. atributy format, width, height)

Příklad použití třídy ImageField

class ImageForm(forms.Form):
    img = forms.ImageField()
form = forms.ImageUploadForm(request.POST, request.FILES)
if form.is_valid():
    img_field = form.cleaned_data["img"]    # instance UploadedFile
    img = img_field.image                   # instance PIL.Image
    metadata = {
        "format": img.format,
        "width": img.width,
        "height": img.height,
    }
    # pokud potřebujeme data, musíme soubor znovu otevřít
    image = PIL.Image.open(img_field)

Session, cookies and local storage

Session (relace, sezení, seance)

Session je v informatice abstraktní pojem představující časově ohraničenou komunikaci mezi aplikací (serverem) a uživatelem (klientem). Session typicky zahrnuje více než jednu zprávu poslanou v každém směru.

Příklady:

  • uživatelská session v operačním systému (přihlášení a odhlášení)
  • session v prohlížeči: uložení otevřených stránek a nastavení při vypnutí prohlížeče
  • na webu:
    • vědomě vytvořená uživatelem (např. přihlášení a odhlášení)
    • sledovaná aplikací bez vědomí uživatele (např. aplikace sledující stránky navštívené daným uživatelem)

Implementace session v síťovém modelu

V kontextu OSI modelu lze session implementovat na různých úrovních:

  • transportní vrstva (transport layer):
    • např. TCP session nebo TCP socket
  • relační vrstva (session layer):
  • aplikační vrstva (application layer):
    • ukládání dat pro reprezentaci stavu a jejich předávání při komunikaci
    • umožňuje programátorovi kontrolu nad rozsahem session, a to i nezávisle na použitých protokolech

Jak přidat stav do webové aplikace?

Protokol HTTP/1.0 byl navržen jako bezstavový, tj. všechny požadavky jsou navzájem nezávislé a nenesou žádná data asociovaná s předchozí komunikací.

Způsoby reprezentace stavu nezávisle na protokolu HTTP:

  • předávání proměnných v URL (komunikace metodou GET), např.
    <a href="/kam/?prom1=h1&prom2=h2...">
    
  • předávání skrytých proměnných ve formulářích (komunikace metodou POST):
    <input type="hidden" name="prom1" value="h1">
    
    použití: např. {% csrf_token %} ve frameworku Django

Třetí způsob využívá přímo protokol HTTP/1.1 – tzv. cookies.

HTTP cookies ("sušenky" 🍪 🍪 🍪)

Cookie představuje malá data, která ukládá prohlížeč na disku klienta dle instrukcí serveru a odesílá je v dalších požadavcích při komunikaci se stejným serverem.

Cookies slouží k uchování stavových informací v jinak bezstavovém protokolu HTTP.

Využití:

  1. správa session v aplikaci (přihlášení, nákupní košík, ...)
  2. personalizace (uživatelské nastavení, nastavení vzhledu (theme), ...)
  3. sledování uživatele (pro zaznamenání a analýzu chování uživatele na webu)

Jak fungují cookies I

Přenos dat obsažených v cookies využívá hlavičky požadavků a odpovědí v HTTP.

  1. Po zpracování prvního požadavku server odešle klientovi odpověď s hlavičkou Set-Cookie

    Obecný tvar: Set-Cookie: <cookie-name>=<cookie-value>

    Např.:

    HTTP/2.0 200 OK
    Content-Type: text/html
    Set-Cookie: yummy_cookie=choco
    Set-Cookie: tasty_cookie=strawberry
    
    [page content]
    

Jak fungují cookies II

  1. Klient data zpracuje a uloží na disku.

  2. V každém dalším požadavku klient odešle všechny dosud nastavené cookies pomocí hlavičky Cookie.

    Např.:

    GET /sample_page.html HTTP/2.0
    Host: www.example.org
    Cookie: yummy_cookie=choco; tasty_cookie=strawberry
    

Parametry cookies

Hlavička Set-Cookie umožňuje serveru specifikovat řadu parametrů pro danou cookie:

Např. Set-Cookie: <name>=<value>; Domain=<domain-value>; Secure; HttpOnly

  • Název cookie může obsahovat libovolné znaky z ASCII, kromě kontrolních znaků (0-32 a 127, tj. včetně všech bílých znaků) a znaků ()<>@,;:\"/[]?={}
  • Hodnota může obsahovat libovolné znaky z ASCII, kromě kontrolních znaků (0-32 a 127) a znaků ",;\. Dále může být volitelně uzavřena mezi znaky " a ". Některé implementace navíc provádějí URL-kódování.

Cookie může být doprovázena volitelnými parametry, které ovlivňují zpracování dat v prohlížeči.

Omezení délky života cookies

Parametry Expires=<date> a Max-Age=<number>:

  • pokud ani jeden není nasteven, jedná se o session cookie (délka života závisí na nastavení prohlížeče – může skončit po zavření okna, nebo nikdy)
  • datum a čas expirace závisí na nastavení hodin klienta!
  • Max-Age má přednost před Expires

Omezení přístupu ke cookies

  • parametr Secure:
    • data v dané cookie lze přenášet jen pomocí protokolu HTTPS
      (aplikace navštívená pomocí HTTP nesmí nastavit Secure cookie a klient nesmí Secure cookie poslat na server pomocí HTTP)
    • omezuje útoky typu man-in-the-middle, ale nezajistí 100% bezpečnost dat
  • parametr HttpOnly:
    • cookie není přístupná z JavaScriptu (pomocí objektu Document.cookie)
    • ale cookie se odešle i v požadavcích, které vznikly v kódu JavaScriptu
    • omezuje útoky typu cross-site scripting

Nastavení rozsahu platnosti cookies dle domény

  • parametr Domain:
    • pokud není nastaven, data se vztahují na přesně stejnou doménu (ne na subdomény)
    • pokud je nastaven, data se vždy vztahují i na všechny subdomény
    • nelze nastavit pro specifickou subdoménu, ale lze nastavit pro doménu vyšší úrovně (např. aplikace na sub.example.org může nastavit cookies pro example.org)
    • nelze nastavit více hodnot

Nastavení rozsahu platnosti cookies dle cesty

  • parametr Path:

    • specifikuje cestu, která se musí vyskytovat v URL, aby se cookie odeslala
    • např. cookie s parametrem Path=/docs:
      • vztahuje se na cesty /docs, /docs/, /docs/Web/, /docs/Web/HTTP
      • nevztahuje se na cesty /, /docsets, /fr/docs
  • parametr SameSite:

    • hodnoty Strict, Lax, None
    • např. cookies s SameSite=Strict jsou odesílány jen s požadavky, které pochází ze stránky na stejné doméně, na kterou se cookie vztahuje

Cookies a bezpečnost

Podle principu fungování cookies nemá server možnost ověřit, jestli data pocházejí z bezpečného zdroje, ani to, odkud data pocházejí.

Např. nějaká zranitelná aplikace na subdoméně může nastavit cookie s parametrem Domain, čímž se data zpřístupní na všech ostatních subdoménách. Navíc ostatní subdomény mohou ovlivnit danou aplikaci pomocí cookies.

Pro zlepšení bezpečnosti standard umožňuje nastavit cookies s prefixy:

  • __Host-* cookies jsou akceptovány prohlížečem, pouze pokud obsahují parametr Secure, přišly v odpovědi přes HTTPS, neobsahují parametr Domain a parametr Path je nastaven na /.
  • __Secure-* cookies jsou akceptovány prohlížečem, pouze pokud obsahují parametr Secure a přišly v odpovědi přes HTTPS.

Cookies a soukromí

Cookies se vztahují na danou doménu (subdomény) a schéma (HTTP nebo HTTPS). Pro danou stránku ale může dojít k přenosu mnoha různých cookies:

  • first-party cookies: pochází ze stejné domény a schématu, jako je URL dokumentu
  • third-party cookies: pochází z jiných zdrojů (např. multimediální data zobrazená na dané stránce), mohou sloužit ke sledování (např. Google Analytics)

Prohlížeče umožňují nastavit blokování všech third-party cookies (viz Firefox, Chrome). Podrobnější filtrování povolených cookies je možné např. pomocí rozšíření (nebo přímo v prohlížeči).

Další funkcí většiny (všech?) prohlížečů je anonymní okno, které je izolované od normálního okna a jehož data se po zavření automaticky smažou.

Firefox navíc umožňuje izolovat různé stránky pomocí přiřazení záložek do kontejnerů.

Cookies vs předpisy EU (a Kalifornie)

Na používání cookies se vztahuje několik globálních legislativních nařízení:

  • General Data Privacy Regulation (GDPR) – EU
  • ePrivacy Directive – EU
  • California Consumer Privacy Act

Vztahují se na všechny weby, které jsou dostupné pro uživatele z daných oblastí. Mezi požadavky patří:

  • informovat uživatele o použití cookies na dané stránce
  • umožnit uživatelům zakázat použití všech nebo některých cookies na dané stránce

V určitých regionech mohou existovat dodatečná, lokální nařízení.

Řešení: zobrazovat zásady používání cookies a tzv. cookie banner.

Zobrazení cookies v prohlížeči

Firefox: Web Developer Tools (Ctrl+Shift+I), záložka Storage.
Chrome: Developer tools (Ctrl+Shift+I), záložka Application.

center

Moderní technologie pro ukládání dat na straně klienta

Cookies byly navrženy jako obecné datové úložiště na straně klienta. Takové použití ale má řadu nedostatků, hlavně pokud jde o bezpečnost a efektivitu. Cookies se posílají s každým požadavkem, takže jejich použití může výrazně zpomalit celou aplikaci (zejména pro pomalé připojení).

Moderní API pro ukládání dat na straně klienta jsou:

Obě technologie využívají JavaScript.

Cookies ve frameworku Django

Základní použití cookies:

  1. objekt HttpResponse má metodu set_cookie()
  2. objekt HttpRequest má atribut COOKIES (slovník obsahující všechny cookies)

Pokročilejší použití s ověřením pravosti:

  1. objekt HttpResponse má metodu set_signed_cookie()
  2. objekt HttpRequest má metodu get_signed_cookie()

Dále má objekt HttpResponse metodu delete_cookie(), která umožňuje smazat cookie z úložiště klienta.

Cookies a dekorátory

Pro práci s cookies se často hodí použít dekorátory jazyka Python.

...viz interaktivní demo...

Další použití cookies ve frameworku Django

Django využívá cookies v dalších částech frameworku:

Viz další přednášky 😄

- [fileinput](https://docs.python.org/3/library/fileinput.html) – pomocný modul pro čtení z několika souborů v jednom `for`-cyklu

- umožnit uživatelům využití našich služeb bez použití cookies

## TODO: [session ID](https://en.wikipedia.org/wiki/Session_ID) - A session ID is typically granted to a visitor on their first visit to a site. It is different from a user ID in that sessions are typically short-lived (they expire after a preset time of inactivity which may be minutes or hours) and may become invalid after a certain goal has been met (for example, once the buyer has finalized their order, they cannot use the same session ID to add more items). [session (web analytics)](https://en.wikipedia.org/wiki/Session_(web_analytics))