Moje odkazy
Obsah článku:
vydáno: 12. 9. 2011 12:05, aktualizováno: 21. 9. 2020 16:04
HTTP je jedním z těch starých dobrých protokolů (jako třeba SMTP nebo POP3), u kterých člověk nepotřebuje žádný zvláštní klientský program a stačí mu telnet (socat, netcat atd.) k tomu, aby se serverem navázal smyslupnou komunikaci a získal od něj odpověď.
V následujícím textu nebudeme objevovat nějaké žhavé novinky poslední doby, ostatně RFC, ze kterých budeme převážně čerpat, pocházejí z let 1996 a 1999, půjde tedy hlavně o opakování základů, které by měl znát každý, kdo se kolem webu pohybuje.
HTTP je aplikační protokol nad transportním protokolem TCP a je založen na principu požadavek/odpověď – klient pošle požadavek (typicky cestu ke stránce, kterou chce) a server mu odpoví. Pro většinu čtenářů to nebude žádná novinka, ale pro pořádek si ukážeme, jak taková komunikace vypadá (černá: telnet klient, červená: náš požadavek, modrá: odpověď serveru):
$ telnet example.com http Trying 2a01:430:2e::161... Connected to example.com. Escape character is '^]'. GET /pokus.xhtml HTTP/1.0 Host: example.com HTTP/1.1 200 OK Server: nginx/0.7.67 Date: Sun, 28 Aug 2011 13:18:10 GMT Content-Type: application/xhtml+xml Content-Length: 153 Last-Modified: Sun, 28 Aug 2011 13:16:54 GMT Connection: close Accept-Ranges: bytes <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title>Pokusná stránka</title> </head> <body> <p>Ahoj světe!</p> </body> </html> Connection closed by foreign host.
Pomocí telnet klienta navážeme TCP spojení se serverem na HTTP portu
(80).
Pomocí nejběžnější HTTP metody (GET) si vyžádáme stránku /pokus.xhtml
.
V klientské HTTP hlavičce Host
uvedeme název domény (jedna IP adresa může obsluhovat více domén – tzv. virtualhosty).
Prázdný řádek značí konec požadavku a následuje odpověď serveru – HTTP hlavičky odpovědi, opět dva konce řádku (CRLF) a samotné tělo odpovědi.
HTTP protokolu bývá někdy vyčítána neefektivita (částečně právem, protože hlavičky odpovědi/požadavku mohou představovat nadměrnou režii), ovšem není pravda, že by kvůli každé dvojici požadavek/odpověď bylo potřeba navazovat nové TCP spojení (což je poměrně drahá operace, při které je potřeba mj. přeložit doménové jméno a vyhodnotit pravidla na všech firewallech po cestě).
Během jedné relace (TCP spojení) můžeme přenést neomezené množství požadavků a odpovědí.
Ve verzi 1.0 HTTP protokolu se k tomu používala (neoficiální) hlavička klienta Connection: Keep-Alive
,
server pak věděl, že po vrácení odpovědi nemá uzavírat spojení, ale čekat na další příkazy.
Ve verzi protokolu 1.1 se všechna spojení považují za trvalá, pokud není uvedeno jinak (hlavičkou Connection: close
).
Trvalá spojení implikují nutnost uvádět délku odpovědi (Content-Length
,
více viz RFC 2616),
protože klient v tomto případě nemůže jednoduše číst data dokud nedojde k zavření spojení
(protože k němu nedojde).
U běžně používaných HTTP démonů (Apache, Nginx atd.) je tato funkce samozřejmostí, ovšem u některých např. testovacích nástrojů, které simulují HTTP server případně klienta, nebo u některých knihoven trvalá spojení nejsou implementována a můžeme pak narazit na problémy s výkonem (např. při nahrávání většího objemu dat přes SOAP či REST rozhraní).
U mnoha stránek nebo aplikací potřebujeme nějak řešit přístupová práva a ověřovat uživatele. Tato funkcionalita je v protokolu HTTP přímo zabudovaná (viz RFC 2617), takže ji můžeme použít, aniž bychom znovu objevovali kolo (vymýšleli vlastní způsob předávání jména a hesla – např. pomocí POST parametrů). Komunikace mezi klientem a serverem pak vypadá takto:
WWW-Authenticate: Basic realm="Prisne tajne"
Authorization: Basic ZnJhbnRhOm1vamUgaGVzbG8=
Přihlašovací dialog v prohlížeči Chromium:
Za výhody HTTP autentizace můžeme považovat:
naopak nevýhodou je:
Tip: jestliže píšete v Javě EE, můžete mezi jednotlivými způsoby autentizace (formulářová vs. HTTP) snadno přepínat – není potřeba upravovat aplikaci, stačí jen změnit konfiguraci aplikačního serveru.
Nenechme se zmást tím, že řetězec ZnJhbnRhOm1vamUgaGVzbG8=
je na první pohled nečitelný
– nejedná se o hash
ale o pouze o data v kódování Base64. Můžeme ho snadno rozkódovat příkazem base64
:
$ echo ZnJhbnRhOm1vamUgaGVzbG8= | base64 -d franta:moje heslo
a získáme jméno a heslo v čitelném tvaru oddělené dvojtečkou. Bez ohledu na to, zda používáme formulářovou nebo HTTP autentizaci, je potřeba komunikaci šifrovat – použijeme protokol HTTPS (HTTP zabalený do SSL/TLS a běžící standardně na TCP portu 443). Jinak by totiž kdokoli, kde „jde kolem“ mohl odposlouchávat hesla uživatelů nebo jim posílat svoje data (podvržené stránky) a tvářit se, že to jsou data z našeho serveru.
Kromě „Basic“ autentizace umí protokol HTTP i „Digest“ autentizaci, při které se neposílá čitelné heslo ale hash, a dále pak možnost pohodlného přihlašování se pomocí klientských certifikátů (jedna z nejbezpečnějších metod).
Klient ve svém HTTP požadavku uvádí URL stránky, o kterou má zájem, ale může svůj požadavek upřesnit a uvést i další svoje přání – ta slouží k vyjednání optimálního formátu dat, který bude vyhovovat serveru i klientovi.
Jistě se vám již stalo, že na vás nějaká stránka, u které jste to vůbec nečekali, začala mluvit česky. Pravděpodobně se jednalo o nějaký redakční systém nebo jiný software, který je lokalizovaný i do češtiny, a jelikož WWW prohlížeč posílá spolu s požadavkem i preferované jazyky, může se aplikace na straně serveru přepnout do správné lokalizace. Preferovaný jazyk se uvádí v hlavičce:
Accept-Language: cs, en-gb;q=0.8, en;q=0.7, fr;q=0.6, de;q=0.5
Jazyků můžeme uvést více a můžeme jim nastavit váhy (platí i pro další hlavičky níže). Výše uvedenou hlavičkou klient serveru říká: dávám přednost češtině, dále pak britské angličtině, jakékoli angličtině, francouzštině, případně němčině. Pro označení jazyků a zemí se používají dobře známé dvoupísmenné kódy: ISO-639 (jazyky) a ISO-3166 (země).
Díky tomu můžeme automaticky lokalizovat ovládací prvky (tlačítka na webu, nabídky atd.), ale i samotný obsah (např. články, popisy zboží), pokud máme daný překlad. Přesto je ale vhodné umožnit i ruční přepnutí jazyka – uživatel může např. sedět u cizího nebo špatně nastaveného prohlížeče.
Kromě jazyka může HTTP klient upřesnit i další údaje. Dříve bylo zvykem poskytovat stránky s různým kódováním češtiny: ISO 8859-2 (tzv. Latin 2), Kódování bratrů Kamenických (KEYBCS2), CP-1250 (Windows-1250) aj. Dělalo se to poměrně neohrabaným způsobem – soubor byl na disku serveru vícekrát v různých kódováních a uživatel musel kliknout na správný odkaz, podle toho, jaké kódování jeho zařízení podporovalo. Dnes sice téměř všude dominuje kódování UTF-8, ale pokud by bylo potřeba poskytovat stránky v různých kódováních, můžeme využít schopností protokolu HTTP. Klient (WWW prohlížeč) odešle požadavek s hlavičkou např.
Accept-Charset: utf-8
a server může stránku dynamicky překódovat do požadovaného tvaru.
Pomocí hlavičky Accept
může klient požadovat určitý MIME typ.
Takže při požadavku na stejné URL, ale s jinou hlavičkou může dostat v případě
Accept: text/plain
stránku převedenou na prostý text, zatímco v případě
Accept: application/xhtml+xml
dostane hypertext.
Stejně tak pomocí této hlavičky můžeme vyjednat se serverem vhodný formát obrázků nebo třeba videa.
Jednou z užitečných vlastností HTTP protokolu je možnost komprese dat. Oceníme ji zejména u textových formátů, jako jsou (X)HTML, CSS, JavaScript. Klient musí dát nejprve najevo, že kompresi podporuje – k tomu slouží hlavička:
Accept-Encoding: gzip, deflate
a server mu pak může poslat komprimovanou odpověď – v hlavičce odpovědi pak musí uvést, jakou kompresi zvolil (klient jich může deklarovat více):
Content-Encoding: gzip
Jelikož komprese se provádí při odesílání stejného souboru pro každého klienta znovu,
umožňují některé HTTP servery jako např. Nginx, abychom měli zkomprimovaný soubor (.gz
) na disku
vedle nezkomprimovaného a server pak pošle klientovi v HTTP odpovědi rovnou ten zkomprimovaný.
V případě hodně navštěvovaných webů tím ušetříme nějaký ten procesorový čas serveru (vhodné pro JavaScripty a kaskádové styly).
Komprese je šikovná, ale musíme si na ni dát pozor, pokud před webový server resp. aplikaci přidáváme ještě nějaké filtry, které např. provádějí dodatečné úpravy textu stránky. V takovém případě musíme proud dat nejprve dekomprimovat a po úpravě zase zkomprimovat – proto je lepší kompresi provádět až v úplně posledním kroku těsně před odesláním klientovi.
Když navšítívíme nějaké webové sídlo a procházíme jednotlivé jeho stránky, většina jeho obsahu – loga, pozadí, JavaScripty atd. – se nemění a bylo by hloupé je stahovat při každém přechodu na novou stránku znovu a znovu.
Klient (prohlížeč) proto může ve svém požadavku poslat hlavičku jako
If-Modified-Since: Thu, 13 Jan 2011 08:06:21 GMT
a server mu pošle daný obsah (např. obrázek), jen pokud byl po tomto datu na serveru změněn.
V opačném případě pošle prázdnou odpověď se stavovým kódem 304 Not Modified
a žádná zbytečná data se nepřenáší.
Prohlížeč pak uživateli zobrazí data z mezipaměti.
Aby to mohlo fungovat, musí klient vědět, jak stará data minule stáhl. K tomu slouží hlavička odpovědi:
Last-Modified: Thu, 13 Jan 2011 08:06:21 GMT
Podle toho se pozná, že má klient staženou stejnou verzi souboru, jako je na serveru, a není potřeba ji přenášet znova.
V případě statických souborů, které servíruje přímo HTTP démon s tímto chováním nebudeme mít žádnou práci – pravděpodobně bude fungovat samo od sebe ve výchozím nastavení. Horší je to s dynamickým obsahem, který generujeme sami.
Dejme tomu, že vyrobíme obrázek, který si budou moci dát naši uživatelé na své stránky, a na něm bude logo našeho programu a pořadové číslo uživatele (nebo třeba člena nějakého klubu). Vezmeme statický obrázek z disku, dopíšeme do něj skriptem nějaký dynamický text a výsledek pošleme klientovi.
Jenže zjistíme, že tento obrázek se generuje pokaždé znovu a vždy se posílá po síti. Objem dat je sice malý a zátěž na serveru také, ale když uživatel stiskne F5 nebo přejde z jedné stránky na jinou, cítí ten rozdíl – zatímco jiné obrázky se vykreslí okamžitě, naše ikona jakoby problikne, je na ní vidět, že se stahovala znovu. Už jen kvůli tomuto pocitu je dobré používat mezipaměť i u takto malých obrázků. Potřebujeme tedy přidat příslušné HTTP hlavičky a kontrolovat, zda má klient poslední verzi, nebo zda mu máme poslat novou. Implementace v PHP může vypadat přibližně takto:
<?php
$soubor = "grafika/logo.png";
$datum = filemtime($soubor);
$logo = imagecreatefrompng($soubor);
$pismo = "/usr/share/fonts/truetype/ttf-dejavu/DejaVuSans.ttf";
$barva = imagecolorallocate($logo, 0xAA, 0xAA, 0xAA);
$id = $_GET["id"]; // FIXME: kontrolovat vstup
$headers = apache_request_headers();
if (isset($headers['If-Modified-Since'])
&& (strtotime($headers['If-Modified-Since']) == $datum)) {
/** Uživatel má načtenou poslední verzi obrázku → nemusíme posílat znovu */
header("Last-Modified: ".gmdate("D, d M Y H:i:s", $datum)." GMT", true, 304);
} else {
/** Uživatel nemá poslední aktuální verzi nebo nemá žádnou → pošleme obrázek */
imagefttext($logo, 8, 0, 15, 20, $barva, $pismo, "Uživatel č.".$_GET["id"]);
header("Content-type: image/png");
header("Last-Modified: ".gmdate("D, d M Y H:i:s", $datum)." GMT", true, 200);
imagepng($logo);
imagedestroy($logo);
}
?>
Skript můžeme vylepšit tak, že nebudeme kontrolovat jen datum poslední změny souboru logo.png
,
ale i samotného PHP skriptu (pro případ, že bychom např. změnili písmo nebo chování programu).
A v javovském servletu můžeme s HTTP hlavičkami pracovat takto:
protected void doGet(HttpServletRequest pozadavek, HttpServletResponse odpoved)
throws ServletException, IOException {
String cesta = zkontrolujParametr(pozadavek.getPathInfo());
File soubor = new File(adresar, cesta);
if (soubor.isFile() && soubor.canRead()) {
if (soubor.lastModified() > pozadavek.getDateHeader("If-Modified-Since")) {
/** Soubor se změnil nebo ho klient ještě nemá načtený. */
odpoved.setContentType(MIME_TYP);
odpoved.setContentLength((int) soubor.length());
odpoved.setDateHeader("Last-Modified", soubor.lastModified());
/** Odešleme data… */
} else {
/**
* Soubor se od posledního načtení klientem nezměnil
* → není potřeba ho posílat znova.
*/
odpoved.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
}
} else {
/** Neexistující nebo nečitelný soubor → HTTP 404 chyba */
odpoved.sendError(HttpServletResponse.SC_NOT_FOUND);
}
}
U statických souborů je používání mezipaměti prohlížeče samozřejmé, navíc to máme bez práce (postará se o něj HTTP démon). U dynamicky generovaných obrázků si podporu dopíšeme. Ale poměrně diskutabilní je využití mezipaměti pro samotné (X)HTML stránky. Jde o to, že mezipaměť nám u nich ušetří minimum prostředků (hypertext zabírá obvykle zlomek toho, co obrázky a další data) a zároveň správná implementace dá nejvíc práce – hlavně u složitějších stránek, které jsou poskládané „z mnoha různých kousků“, je velice pracné nebo až téměř nemožné zjistit datum poslední aktualizace celku. Obvykle tedy dojdeme k závěru, že takové řešení je neekonomické – buď bychom strávili neúměrně mnoho času implementací, nebo bychom dosáhli nedokonalého řešení, které bude pouze mást uživatele a způsobovat problémy.
Kromě výše zmíněných hlaviček Last-Modified
a If-Modified-Since
máme ještě další možnost:
místo data můžeme použít hash/otisk dat: ETag: "686897696a7c876b7e"
a klient se pak při opakovaném požadavku ptá s hlavičkou If-None-Match: "686897696a7c876b7e"
,
na kterou opět může dostat odpověď 304 Not Modified
, pokud se data na serveru nezměnila.
Většinu souborů přenášených HTTP protokolem (stránky, obrázky, ISO obrazy atd.) má smysl poslat po síti jen vcelku od začátku do konce. Jinak je tomu ale u multimediálních dat (filmy, hudba). Uživatel totiž může chtít skočit doprostřed záznamu, aniž by čekal, až se stáhne celý soubor. Opět nemusíme vymýšlet vlastní řešení (např. předání požadovaného úseku souboru přes GET parametry) a vystačíme si s vlastnostmi protokolu HTTP.
Požadavek může obsahovat hlavičku Range, ve které uvedeme, o jakou část souboru máme zájem. Např. pomocí hlavičky:
Range: bytes=500-999
si vyžádáme druhých 500 bajtů ze souboru na serveru.
Výhodou tohoto řešení je jeho standardizovanost – na straně klienta nemusí být nějaký náš speciální program, můžeme použít třeba multimediální přehrávač VLC (nebo video přehrávat rovnou v prohlížeči) a uživatel může přistupovat k náhodným částem filmu, aniž by ho musel stahovat celý.
Další využití má hlavička Range
v případě stahování velkých souborů
– pokud nám např. spadne WiFi spojení, nemusíme soubor stahovat celý znovu, ale navážeme ho – některé webové prohlížeče to umí.
Stahujeme-li soubor programem wget, slouží k tomu přepínač -c
:
wget -c http://example.com/velky-soubor.tar.gz
Zde můžeme narazit na jedno úskalí: co když se soubor mezi tím na serveru změnil a my teď stahujeme druhou půlku jiné verze, než kterou jsme stahovali předtím? Výsledkem pak bude nekonsistentní a pravděpodobně nepoužitelný soubor.
Můžeme si pomoci hlavičkou If-Unmodified-Since
– soubor stáhneme, jen pokud nebyl od určitého data měněn.
Nebo můžeme integritu souboru zkontrolovat dodatečně – pokud nám server pošle jeho hash:
Content-MD5: Q2hlY2sgSW50ZWdyaXR5IQ==
Algoritmus MD5 je sice zastaralý a z bezpečnostního hlediska překonaný, ale pro potřeby kontroly integrity souboru (kde nepředpokládáme žádný zlý úmysl) ho stále můžeme používat.
Podpora Range
hlavičky na straně HTTP démonů bývá dobrá a obvykle ve výchozím nastavení jednoduše funguje.
To se týká servírování statických souborů – pokud ale soubory poskytujeme skrze nějaký náš vlastní skript nebo třeba servlet
(např. je načítáme z databáze nebo programově ověřujeme uživatelská oprávnění),
musíme podporu Range
implementovat sami.
Někdy nechceme, aby se obsah (obrázek, PDF dokument atd.) po kliknutí na odkaz uživateli neotvíral ve webovém prohlížeči, ale aby se zobrazil dialog pro uložení souboru na disk. V HTTP odpovědi můžeme klientovi poslat následující hlavičku:
Content-Disposition: attachment; filename=soubor.png
Ta se postará jednak o zobrazení souborového dialogu a jednak umožňuje nastavit nějaký přívětivý a smysluplný název (bez toho by pravděpodobně prohlížeč použil název skriptu a špatnou příponu).
V úvodu jsme si uvedli, že pro experimentování s HTTP protokolem nám stačí pouhý telnet klient, což je sice pravda, ale není to zrovna dvakrát pohodlné – můžeme proto využít i sofistikovanější nástroje.
Asi nás nebude bavit věčně psát:
GET /soubor.txt HTTP/1.1 Host: example.com
a vypisovat další hlavičky.
Místo telnetu použijeme Socat – to je zjednodušeně řečeno nástroj, který umožňuje spojit dva konce libovolných „rour“
a nechat mezi nimi proudit data.
Jedním koncem bude v našem případě TCP soket a druhým koncem standardní vstup/výstup (ale Socat toho umí daleko více – viz man socat
).
Takto můžeme pomocí socatu zhruba emulovat chování telnet klienta:
socat TCP:example.com:80 STDIO
Ale tím jsme si nijak nepomohli – příkazy musíme stále vypisovat ručně a vždy znovu. Kouzlo tkví v tom, že jako druhý konec můžeme místo STDIO napojit GNU Readline:
socat TCP:example.com:80 READLINE,history=$HOME/.http_historie
A teď nám zde bude fungovat historie příkazů podobně jako v BASH shellu
– příkazy můžeme listovat pomocí šipek nahoru a dolů,
velmi užitečné je také vyhledávání v historii (Ctrl+R),
kde stačí napsat kousek dříve zadaného příkazu a stisknout Enter pro jeho odeslání.
Historie příkazů se bude ukládat do souboru ~/.http_historie
, takže se k ní můžeme vracet i po odpojení od serveru.
Dalšími oblíbenými nástroji jsou Wget a curl – zde už nemusíme ručně vypisovat příkazy a hlavičky HTTP protokolu, ale jen zadáme URL a případně další parametry a nástroj provede tuto práci za nás. Stažení stránky wgetem s ručním zadáním jedné hlavičky:
wget --header='Accept-Language: cs' http://example.com/index.xhtml
Nebo pomocí curlu můžeme posílat data metodou POST – např. komunikovat s webovou službou:
curl -H "Content-Type: text/xml" -d @požadavek.xml http://example.com/webovaSluzba
Možnosti obou programů jsou takřka neomezené a nemá smysl je tu všechny vypisovat – viz man wget
a man curl
.
Pro starší verze prohlížeče Firefox bylo potřeba instalovat rozšíření Firebug. Dnes je podobný nástroj součástí většiny prohlížečů už ve výchozí instalaci a lze ho vyvolat klávesovou zkratkou F12.
Firebug se hodí pro zkoumání kódu stránky a provádění okamžitých zásahů do DOMu, CSS stylů a JavaScriptu.
Další jeho užitečnou funkcí, která nás teď bude zajímat nejvíc, je sledování síťového provozu.
Díky ní se můžeme podívat na konkrétní hlavičky jednotlivých HTTP požadavků a odpovědí a pohodlně ladit.
Na následujícím obrázku např. vidíme, že většina odkázaných souborů (obrázky, styly atd.)
se nemusela ze serveru při obnovení stránky stahovat – server pouze odpověděl 304 Not Modified
a webový prohlížeč použil dříve stažený soubor ze své mezipaměti:
Firebug nám umožní dívat se i do šifrované HTTPS komunikace (jelikož prohlížeč přirozeně má k dispozici i nezašifrovaná data), ke které bychom se jinak nedostali (museli bychom použít vlastní proxy server, který by komunikaci šifroval/dešifroval). Ale jelikož HTTP klientem nebývá zdaleka jen webový prohlížeč, bude se nám k ladění hodit i následující nástroj:
Wireshark (dříve Ethereal) a jeho kamarád tshark jsou nástroje pro odchytávání síťového provozu a jeho následnou analýzu.
Jelikož síťový provoz nemůže zachytávat obyčejný uživatel (to by byla velká bezpečnostní díra)
a jsou potřeba práva superuživatele (roota), museli bychom pustit Wireshark přes sudo (gksudo
, kdesudo
…).
Ale vhodnější bude pustit pod rootem jen program tshark
,
který odposlechne komunikaci a předá ji do pojmenované roury (zde jsme ji nazvali shark
).
Z této roury pak budeme data číst Wiresharkem, který už běží s právy obyčejného uživatele:
$ mkfifo shark # tshark -w - > shark $ wireshark -k -i shark
Kromě jiného to má výhodu v tom, že oba programy můžou běžet na různých počítačích – tshark
pustíme třeba na vzdáleném směrovači nebo serveru
a Wireshark hezky u sebe na desktopu (k přesměrování proudu dat použijeme SSH místo lokální pojmenované roury).
Jelikož dat přenášených po síti může být poměrně hodně, je dobré data filtrovat už na vstupu při zachytávání. Např. takto si vyfiltrujeme jen provoz na TCP portu 80 (HTTP):
tshark -f "tcp port 80" -w - > shark
Další možnosti filtrování jsou v GUI Wiresharku.
Takto vypadá HTTP komunikace zachycená Wiresharkem:
O Wiresharku by se dal napsat celý článek nebo seriál, proto se zatím spokojte s touto ochutnávkou.
Protokol HTTP, který původně vznikl pro přenos hypertextových stránek, se dnes díky svému dobrému návrhu používá i k mnoha jiným úlohám (i když někdy můžeme hovořit o nadužívání). Je totiž zároveň jednoduchý i mocný – v dnešním článku jsme si ukázali např. možnosti vyjednávání vhodného formátu mezi klientem a serverem, efektivní datové přenosy (nepřenášení již přenesených dat), nebo náhodný přístup k částem souborů (vhodné pro audio/video záznamy nebo obnovu přerušeného stahování).
Témata: [www]
Tento článek zatím nikdo nekomentoval