HTTP

Robert Gemrot, 525262@mail.muni.cz

Pojmy

Popis protokolu

Charakteristika

HTTP byl navržen pro přenos dokumentů (např. HTML stránek) po internetu. Od té doby se vyvinul, a jelikož většina middleboxů HTTP provoz propouští, používá se dnes na spoustu věcí (př. REST API nebo frontend pro konfiguraci všeho možného).

  • Klient-server architektura -- klient se ptá serveru (HTTP request) a server mu odpoví (HTTP response).
  • Bezestavový -- server neudržuje žádné informace o uživateli mezi dvěma požadavky.

    Ale: cookies (server řekne klientovi, ať mu posílá tento kousek dat s každým dalším requestem)

  • Textově založený -- ve zprávách (požadavky a odpovědi) se používá plain ASCII text, takže je lidsky čitelný.

Connection Management

Původně (HTTP 1.0): 1 HTTP request = 1 nové TCP spojení

Problém 1: TCP handshake něco trvá. TCP používá model AIMD (nebo podobný), zkrátka na začátku je rychlost přenosu pomalá a postupně roste.

Řešení v1 (HTTP 1.1): budeme používat znovu TCP spojení po předchozím requestu. Hlavička Connection: Keep-Alive

Problém 2: Po loadu stránky vím, že chci poslat 20 HTTP dotazů (CSS, JS, IMGs). Nechci otevírat 20 studených TCP spojení, chci použít to jedno teplé. Ale zároveň to chci paralelně.

Řešení 2: sypu všechny dotazy najednou do jednoho TCP spojení, a pak čekám na odpovědi. Velmi těžké na implementaci, nepoužívá se defaultně.

Problém 3 (head-of-line blocking): Server odpovídá postupně. První odpověď blokuje zbytek.

Revize protokolu HTTP

  • HTTP 2
    Binární protokol, nemění význam HTTP 1.1, pouze způsob přenosu. Server může odpovědět více odpověďmi, než na co se klient ptal (např. potřebné CSS a JS soubory). Taky řeší třetí problém použitím pravého multiplexingu.

    Více informací viz https://developer.mozilla.org/en-US/docs/Glossary/HTTP_2

  • HTTP 3
    Používá QUIC (TLS + UDP) místo TCP. Toto nějak obchází head-of-line blocking na transportní (TCP/UDP) vrstvě sítě; detaily jsem nezjišťoval.

Přehled Komunikace

Nejdříve musí klient poslat HTTP dotaz. Každý HTTP dotaz se skládá z:
  • Metody, cesty, verze HTTP na prvním řádku (GET /index.html HTTP/1.1)
  • Seznamu hlaviček -- hlavička je key-value pár (Accept-Language: cs-CZ).
  • Prázdného řádku pro oddělení -- používá se CRLF!
  • Těla požadavku -- např. data z formuláře, nebo data pro vytvoření. Typické pro metody POST, PUT (pro REST API často v kombinaci s hlavičkou Content-Type: application/json).
HTTP server typicky poslouchá na portu 80 (HTTPS na 443). Server přijme požadavek a odpoví na něj HTTP odpovědí. HTTP odpověď se skládá z:
  • Verze HTTP, stavový kód a popis na prvním řádku (HTTP/1.1 200 OK).
  • Seznamu hlaviček -- hlavička je key-value pár (Content-Type: text/html).
  • Prázdného řádku pro oddělení
  • Těla odpovědi -- např. HTML stránka, nebo data v JSONu. Typické pro metody GET. I při metodě POST se může vrátit např. nově založený záznam.
Na příkladu:
curl https://www.fi.muni.cz/~kas/pv090/referaty/2023-podzim/dns.html
          GET /~kas/pv090/referaty/2023-podzim/dns.html HTTP/1.1
          Host: www.fi.muni.cz
          User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/118.0
          Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
          Accept-Language: cs,sk;q=0.8,en-US;q=0.5,en;q=0.3
          Accept-Encoding: gzip, deflate, br
      
          HTTP/1.1 200 OK
          Date: Fri, 20 Oct 2023 11:48:41 GMT
          Server: Apache
          Last-Modified: Mon, 16 Oct 2023 08:14:24 GMT
          Keep-Alive: timeout=5, max=100
          Connection: Keep-Alive
          Content-Type: text/html; charset=utf-8
          Content-Language: en

          ...tělo odpovědi
      

Metody: GET, PUT, PATCH (idempotentní), POST, OPTIONS, DELETE

Stavové kódy odpovědí -- viz https://http.cat/

Range request

Pokud server podporuje (na nějakém konkrétním souboru/stránce) range request, klient je schopný si vyžádat jen část dat. To se používá například v přehrávačích nebo při stahování souborů (možnost pozastavit stahování).

Ukázka:

            Klient:
            curl --head http://i.imgur.com/z4d4kWk.jpg

            Server:
            HTTP/1.1 200 OK
            …
            Accept-Ranges: bytes
            Content-Length: 146515

            Klient:
            GET /z4d4kWk.jpg HTTP/1.1
            Host: i.imgur.com
            Range: bytes=0-1023

            Server:
            HTTP/1.1 206 Partial Content
            Content-Range: bytes 0-1023/146515
            Content-Length: 1024
          

Content Negotiation

Obecně HTTP definuje pouze obecný pojem "content-negotiation" a dává k dispozici nějaké hlavičky a stavové kódy, které lze použít. Tyto hlavičky obecně začínají Accept* a používají se pro vyjádření preferencí klienta.

Dostupné a používané hlavičky:
  • Accept -- MIME typ, co klient chce
  • Accept-Encoding -- jakým kompresním algoritmům klient rozumí
  • Accept-Language -- v jaké jazykové mutaci chceme obsah
  • Accept-Charset -- v jaké znakové sadě (př. UTF8) chceme obsah

Konkrétní mapování těchto hlaviček v chování HTTP Serveru je specifické pro každou implementaci. Server by měl poslat "Vary" hlavičku, ve které popíše, které mechanismy opravdu podporuje -- aby potom cache věděla, že změna v nepoužívané hlavičce nevadí.

Pro informace ohledně Apache: https://httpd.apache.org/docs/current/en/content-negotiation.html

CGI skripty a pooling

Motivace: Potřebuji generovat dynamický content (třeba závisející na aktuálním čase). Je blbost dávat do webového serveru nějaký samostatný programovací jazyk. Místo toho webový server může na základě URL poznat, jaký skript má pustit, udělat nový proces a jeho standardní výstup použít jako odpověď pro klienta.

Charakteristiky:
  • Flexibilní -- skript v jakémkoliv programovacím jazyce.
  • Rozšiřitelné -- skript může generovat cokoliv, třeba PDF nebo JPG.
  • Pomalé -- vždy spouštím nový proces.

Pomalost se řeší klasickou technikou "bazénkování" (pooling, neplést s polling) -- stejně jako v kontextu backendového programování existuje pool otevřených spojení do databáze (ze kterého si vždy jen nějaké "vypůjčíme"), abychom nemuseli vždy otevírat a zavírat (což je drahé), i zde se vynalezly přístupy, jak "bazénkování procesů" dosáhnout.

Jednou z klasických možností je PHP, kdy při startu webového serveru instanciujeme bazének procesů (PHP interpreterů), a poté si vždy jen daný interpret "vypůjčíme" pro zpracování daného requestu.

Velmi podobná věc: ASP.NET (a skoro jakýkoliv jiný backendový HTTP framework) má pool worker threads, kterým se vždy konkrétní požadavek přiřadí, ale po zpracování se zařadí zpátky do bazénku.

Cachování

Cachování se v HTTP děje na několika úrovních. Hlavní z nich jsou:
  • In-Browser cache (under Private caches) -- Browser podle hlaviček vrací starou verzi dokumentu
  • Proxy cache (under Shared caches) -- specializovaný server jako proxy zastupující ten hlavní HTTP server

HTTP-EQUIV

Prohlížeč posílá GET požadavky na danou URL. Lze se podívat v Network tabu. Umí posílat POST (`form` tag). Ostatní metody -- využít fetch API. Parsuje odpověď a zobrazí ji uživateli nějak vyrenderované. Pokud je to HTML, tak tam můžou být "meta" tagy s HTTP-EQUIV direktivami. Tyto simulují HTTP hlavičky odpovědi. Prohlížeče jim rozumí. Jenže jim např. nerozumí cachovací proxy servery (museli by parsovat HTML). Pokud můžete použít HTTP hlavičky, použijte ty.

Use-case, kdy použít HTTP-EQUIV:
  • Nemůžete konfigurovat HTTP server (light hosting)
  • Až na klientovi z JS potřebujete zpětně přidat hlavičku
Více informací: https://rviscomi.dev/2023/07/you-probably-dont-need-http-equiv-meta-tags/

Caching Best Practices

Důležité! Nasadíte novou verzi Reactí aplikaci, ale uživatelé si stěžují, že styly jsou rozbité. Build systém doporučí, abychom nastavili hlavičky "Cache-Control: max-age=31536000" pro věci ve složce static, "Cache-Control: no-cache" pro ostatní, my jsme to ale neudělali, a tak se cachují i věci, které se cachovat nemají.

Když konfigurujeme webserver, je důležité se zamyslet, jakou strategii zvolíme pro cachování. Například aby prohlížeč nebo cache proxy věděli, jak dlouho si mají data uchovat. Aby se nestalo, že při nasazaní nové verze aplikace mám ještě staré zacachované CSS styly, ale už nový HTML dokument.

  • Dlouhá životnost cache, nikdy neměním obsah souboru -- cache se prostě použije
  • Měním obsah souboru, nulová životnost cache -- nechat cache konzultovat server, jestli je obsah OK
Je několik HTTP hlaviček, které tohle ovlivňují:
  • Z HTTP serveru: Cache-Control, Expires, ETag, Last-Modified
  • Z klienta: If-None-Match, If-Modified-Since
Zdroj: https://jakearchibald.com/2016/caching-best-practices/

Autentizace

Budeme se bavit o tom, že se klient (browser) chce prokázat přímo webovému serveru, na kterém je resource.

Jiné často používané možnosti jsou např. OAuth2 (OpenId Connect), kdy se klient prokazuje nějakému serveru třetí strany (Google / Microsoft / ...), který mu pak vydá token, a odkáže ho zpět na resource server, který token ověří. Kromě OpenId Connect se využívá např. SAML (security assertion markup language) založený na XML. Otázka: který přístup k implementaci SSO používá MUNI?

Přímo v HTTP protokolu

Různé schémata: Basic, Digest, Bearer, ...

Stejná struktura: Klient se zeptá, server řekne 401 Unauthorized, klient zadá potřebné údaje a pošle request znovu s Authorization header

  • Basic Authentication: base64 kódování
  • Digest: používá hash
  • Bearer: jiné RFC, stejný princip

Následující sekce už se typicky neautentizují přímo vůči webovému serveru, ale vůči nějakému CGI skriptu obsluhujícím daný endpoint (resource). O to více jsou ale pro praktické použití důležité, protože dnes se většina webových aplikací dělá v nějakém frameworku, který poskytuje nějaké tyto možnosti.

Pomocí cookies

Klient se přihlásí, server mu pošle cookie, klient ji při každém dalším requestu pošle zpátky. Server si cookie někde uloží a přiřadí ji k nějakému uživateli.

Problém: Klient může cookie upravit, nebo si je vymyslet. Řešení: podepsat cookie, aby klient nemohl její obsah upravit.

HttpOnly a Secure flags pro zvýšení bezpečnosti cookies

JWT

Je to pouze specifikace formátu, v jakém přenášet nějaké tokeny.

Server vygeneruje JSON token se sadou "claims" o uživateli a podepíše to (symetricky - (H)MAC - nebo asymetricky - digital signature)

Do claims můžu zahrnout i informace pro frontend -- jaké ID a role uživatel má (podmíněný rendering u client-side aplikací)

Klient může posílat v custom hlavičkách, nebo třeba jako Authorization: Bearer token.

Dostupný software

Konfigurace Apache

Základní konfigurace

  • hlavní soubor /etc/httpd/conf/httpd.conf (fedora, jinde zde)
  • soubory .htaccess přímo v adresářích s obsahem
  • "Direktiva": Tvrzení o konfiguraci, co má platit. Umístit podle chtěného scope.
  • DocumentRoot direktiva - místo ve filesystému, kam bude apache mapovat URLs.

    For example, if DocumentRoot is set to /var/www/html and a request is made for http://www.example.com/work/, the file /var/www/html/work/index.html will be served to the client. https://httpd.apache.org/docs/2.4/getting-started.html

  • VirtualHost direktiva umožňuje více doménových jmen namapovat na tento jeden fyzický stroj

Direktivy a jejich use-cases

VirtualHost direktiva

Použití: Chci, aby na jednom fyzickém serveru běželo více oddělených webů.

Například: Dotaz http://one.example.com/test.html mapovat jinam než http://two.example.com/test.html

Řešení:
<VirtualHost *:80>
    ServerName one.example.com
    DocumentRoot /var/www/one
</VirtualHost>

<VirtualHost *:80>
    ServerName two.example.com
    DocumentRoot /var/www/two
</VirtualHost>
            

Konfigurace CGI skriptů

  1. Použití LoadModule cgid_module modules/mod_cgid.so v httpd.conf
  2. Povolení CGI skriptů v daném adresáři pomocí
    <Directory "/var/www/cgi">
        Options +ExecCGI
    </Directory>
                    
  3. Řeknu Apachovi, které všechny soubory má považovat za CGI skripty (podle přípony)
                      AddHandler cgi-script .cgi .py
                    

Multiviews, odpověď podle preferencí klienta

Problém: Podle hlavičky Accept-Language chci vrátit jiné soubory v dané jazykové mutaci

Řešení: pomocí MultiViews
  • Buď manuálně specifikovat type map
  • Nebo nechat server automaticky "generovat" type map

Autentizace

Návod pod odkazem je dobrý, důležité části jsou:
  • Konfirurace Directory, který má být chráněný
  •                 AuthType Basic
                    AuthName "Restricted Files"
                    # (Following line optional)
                    AuthBasicProvider file
                    AuthUserFile "/usr/local/apache/passwd/passwords"
                    Require user rbowen
                  
  • Nastavení jména a hesla pro basic Authentication do daného souboru

SSL (HTTPS)

Viz návod pod odkazem, části:
  • jaký port (443), doména...
  • kde se nachází certifikát, kterým se webserver prokazuje
  • kde se nachází klíč, který webserver má používat

Redirects

  • RewriteRule -- podporuje regulární výrazy, server odpovídá rovnou 200 OK jakoby k redirectu nedošlo
  • Redirect -- jednodušší, server klientovi řekne "tady nic není, ale hledej tady na té URL"

Literatura