Firmy IT go nie cierpią, ujawnił pytania rekrutacyjne dotyczące HTTP. Uwaga.
- Co znaczy, że http jest protokołem bezstanowym.
- Co to Cache-Control i czy no-cache oznacza że nie keszujemy??////~~
- Czym się różni PUT od POST
- Co to persistent connection
O nic więcej nikt Cię nie zapyta, bo umówmy się, nic więcej zazwyczaj o HTTP nie wiemy 🙂
(wiadomo jednak, że pierwszym, najgorętszym i najbardziej istotnym tematem na każdym interwju w dynamicznej firmie jest garbage collector i słówko kluczowe volatile).
Mógłbym tu skończyć. Ale my chcemy w prostych słowach ogarnąć podstawy HTTP.
RFC2616 HTTP
Jak czytam RFC to po 5 minutach mam otwartych 17 zakładek z pokrewnymi RFC i innymi stronami z internetu co tłumaczą mi co w tych RFC jest napisane. Jacyś ostrzy mędrcy napisali te RFC. Wielu z nas natomiast pisze aplikacje webowe, tworzymy mniej lub bardziej nieudolnie różne API. Warto żebyśmy wiedzieli cokolwiek co w tych RFC jest popisane.
Przypuśćmy, że tworzysz API dla swoich dwóch kolegów programistów. Jeden Francuz, drugi z Radomia (<3). Wywołanie Twojego API zwraca informacje o pogodzie. Francuz sprytny programista chce czytać po francusku. Patrzy do RFC2616. Mhm, okej… jak wpiszę w requestcie Accept-Language: fr to teoretycznie API serwera powinno mi odpowiedzieć danymi w języku francuskim. O ile (!) programista klikający API również ogarnia kapkę HTTP i wyłuska w kodzie nagłówek Accept-Language i go obsłuży. A radomianinowi wszystko jest jedno, bo jest dobry ogar i dla niego może być i w takim języku i owakim. Więc wpisuje sobie Accept-Language: pl. No i fajnie. Dzięki RFC wiemy jak rozmawiać ze sobą. Tzn., jak ma rozmawiać klient (np.: Ty obsługujący przeglądarkę w swoim domu w słonecznym centrum Radomia) z serwerem, który jest gdziekolwiek.
Request & Response
Na komputerze uruchamiasz procesy. Proces, czyli uruchomiony program. Mówiliśmy już o tym przy okazji posta o wielowątkowości. Np.: przeglądarka internetowa. Przez przeglądarkę wchodzisz na strony ze śmiesznymi obrazkami i nie tylko śmiesznymi. Ze wszystkimi tymi stronami komunikujemy się dzięki protokołowi HTTP, który umożliwia wysyłanie i odbieranie komunikatów (request/response) będącymi podstawową jednostkę komunikacji HTTP. Wysłanie komunikatu typu request/żądanie można wywołać poprzez wciśnięcie entera w pasku przeglądarki po wpisaniu adresu strony. A to co potem cieszy naszą duszę, czyli strona internetowa, którą widzimy na ekranie jest odpowiedzią na żądanie/response. Żądanie do serwera, który obsługuje komunikaty HTTP możemy wysłać przez wspomnianą przeglądarkę, ale nie tylko. Możesz to zrobić przez telnet, curla, Postmana czy innego Burpa.
Hypertext
HyperText Transfer Protocol. Co to jest ten hypertext w ogóle? A na przykład ten oto odnośnik. Czyli jest to słowo, które prowadzi do innego dokumentu, do jakiejś innej strony HTML. Każde kliknięcie odnośnika, zaprowadzi nas do URLa (hyperlink) i spowoduje wysłanie nowego żądania HTTP do serwera. Pisząc to czuję się jak na lekcji informatyki w gimnazjum. Jedźmy dalej. Będzie równie emocjonująco.
Jak to działa
RFC2616 opisuje kilkanaście terminów, którymi autorzy posługują się w całym dokumencie. Kilka słów o nich poniżej. Oznaczam je kursywą.
Jak już mogłeś odnieść wrażenie, protokół HTTP bazuje na komunikatach request oraz response. Zarówno request jak i response mogą zawierać w sobie entity. Czyli payload / ładunek / informacje, które transmitowane są z od klienta do serwera i na odwrót. Ten ładunek składa się z nagłówków i z contentu / treści, które są przekazywane przez sieć. Nagłówki opisują cechy tej treści (np.: Content-Length) albo cechy zasobu, np.: Content-Encoding w responsie (są też nagłówki które tego nie robią, np.: nagłówek Date, ale pomińmy to). Jest jeszcze termin message-body stanowiący treść naszego żądania lub odpowiedzi. Uznajmy na tę chwilę, że message-body jest równoważnym terminem z entity-body. Co tam jeszcze… Nieraz jest tak, że pobierasz jakieś dane i możesz wybrać w jakim formacie one mają być, np.: XML albo JSON. Albo w jakim języku. Te obie funkcje możliwe nieraz są z poziomu GUI (wybierasz język / format z listy rozwijanej). Aaaalbo sam wysyłasz takie żądanie przy pomocy np.: Curla. Więc pobieramy te same dane, ale w różnej formie, w zależności od tego, jakie są Twoje preferencje. I tu pojawia się termin representation. Czyli po prostu nadanie formy zwracanemu entity. np.: format application/json oraz język polski. Z pojęciem reprezentacji związany jest proces content negotiation, o czym kilka akapitów niżej.
Wracając do requesta. Ty, a dokładnie np.: Twoja przeglądarka jesteście klientem, który poprzez wpisanie nazwy strony wysyła żądanie, czyli request. My strzelmy sobie do jakiegoś API z Postmana.
Wyraziliśmy chęć pobrania zasobu (resource – dane które są identyfikowalne poprzez URI, o czym niedługo) przy pomocy metody GET. Serwer udostępniając nam zasoby przy pomocy metody GET jasno daje klientowi do zrozumienia, że jest to endpoint, który zwróci nam dane. O intencjach metod HTTP będę jeszcze pisał.
Na końcu żądania widoczny jest znak nowej linii. Bez tego nie dostaniemy odpowiedzi. Teraz sobie przypominam ile razy telnetowałem się w życiu na port 80 wysyłając jakieś żądania i zawsze zastanawiałem się, czemu jeden enter (po ostatnim nagłówku) nie starcza. Tzn., nie zawsze się zastanawiałem, ale kilka razy się myślę zdarzyło. W zasadzie to nadal nie znam odpowiedzi. Chyba tak HTTP ma i już (w RFC jest o tym napisane gdzieś).
Przelećmy po tych podstawowych nagłówkach.
Content-Type & MIME sniffing
Pierwsze RFC, które wspomina o tym nagłówku jest z 1988 roku (!). Leci czas. Nagłówek ten posłuży nam do zdefiniowania postaci / formy / natury danych, które przesyłamy w ciele żądania oraz do powiadomienia klienta (np.: przeglądarki) jaki typ danych wraca wraz z responsem. Mówiąc wprost, nagłówek ten może znajdować się zarówno w requestcie jak i w responsie.
Content-Type składa się z typu i podtypu. Na przykład:
Content-Type: image/jpeg
Albo:
Content-Type: application/json
Mamy tu ogólny / główny typ: image i określenie jego podtypu: jpeg. No proste całkiem.
Załóżmy, że do aplikacji możesz załadować pliki pdf. Filtrujemy po stronie serwera requesty po Content-Type tzn., jeśli jest inny niż application/pdf to odrzucamy request. Rabuś-haker jest niebezpieczny i wysyła nam HTMLa (html zdecydowanie nie jest pdfem) z groźnym kodem JavaScript. Dlatego ustawił spryciulka Content-Type requesta na application/pdf tak jak oczekujemy. Nasza aplikacja nie używa innych metod filtrujących. Plik HTML rabusia z najgroźniejszym atakiem XSS tzn.: <script>alert(‘1’);</script> ląduje na serwerze (gorzej jak będzie tam JavaScript, który wywołany po uruchomieniu HTMLa przez administratora wyśle wrażliwe dane na serwer rabusia).
Po otwarciu takiego pliku z rozszerzeniem HTML na Chrome i na IE złośliwy skrypt zostaje wywołany. Nie jest to duża niespodzianka.
Po otwarciu pliku bez rozszerzenia, na Chrome dostaję zawartość pliku jako plain text, natomiast na IE skrypt zostaje wywołany.
Nie ustawiłem jednak w kodzie mojego prostego servletu nagłówka Content-Type dla odpowiedzi. Zatem Ty i Twoja przeglądarka, nie wiedząc z jakim typem danych macie do czynienia próbujecie to wyniuchać. Przeglądarka ma w sobie jakieś magiczne algorytmy, które potrafią robić taką detekcję typu. O MIME sniffingu możesz przeczytać tu.
Gdy ustawiłem Content-Type w responsie na application/pdf, to wszelkie próby ataku spełzły na niczym. Fajnie, coraz mądrzejsze te przeglądarki.
A co jeśli uruchomiłbym ten plik na IE6? Sprawdziłem to na Windowsie XP 🙂 Mimo ustawienia w responsie ContentType’u przeglądarka wykorzystuje swoje możliwości sniffingu typu danych, które wracają z serwera.
“O. HTML! To wyświetlam, mimo że ContentType response’a to application/pdf.”, Przeglądarka, Zakopane 1992
Dlatego Microsoft, a inne przeglądarki (podobno nie wszystkie, np.: Safari) poszły tym samym śladem i dodały obsługę nagłówka X-Content-Type-Options (obsługa od IE8 z tego co wyczytałem), który z wartością nosniff wyłącza możliwość sniffowania typu danych przez przeglądarkę. Kontrola najwyższą formą zaufania. Jeśli deweloper aplikacji określił w responsie ContentType: application/pdf to tak ma być i finito. Przeglądarka nie będzie mieć tu nic do gadania i nie powinna próbować wyświetlić podrzuconego podstępem HTMLa.
Gdy w Chrome _nie_ ustawię nagłówka Content-Type, a jednocześnie ustawię X-Content-Type-Options: nosniff, to przeglądarka nie wyświetli już mojego HTMLa, tylko wyświetli zawartość strony HTML jako plain/text. Czyli zgodnie z opisem działania nagłówka X-Content-Type-Options – przeglądarka nie dostała konkretnego Content-Type z serwera i jednocześnie zabroniono jej detekcji typu zwracanych danych. Nie wiedziała co zrobić więc wyświetliła tekst.
Accept
Jeśli nie zdefiniujemy sami nagłówka Accept w żądaniu, to do serwera trafi:
Accept: */*
Akceptujemy zatem wszystkie media types. Według specyfikacji HTTP, jeśli serwer nie jest w stanie sprostać żądaniu klienta umieszczonego w nagłówku Accept to powinien zwrócić kod 206 (not acceptable).
Pewnie widziałeś, podglądając żądanie np.: w Fiddlerze, że w nagłówku Accept jest wiele wartości.
Accept: text/html,application/json,application/xml;q=0.9
Daj mi HTML, a jeśli nie możesz to zadowolę się JSONem, ewentualnie w ostateczności XMLem.
Format wygląda tak:
Accept: typ1/subtyp1<średnik jeśli parametr> parametr (np.: q <0,1>), (...), typ2/subtyp2, ... , typN/subtypN
Każdy typ (np.: text/html) ma swój parametry jakości (?). Defaultowo jego wartość to 1. Jeśli ustawisz nagłówek Accept jako:
Accept: application/json;q=0.9,application/xml;q=0.8,text/html
To serwer, gdyby miał algorytm polegający na weryfikacji nagłówka Accept stwierdziłby, że klient najbardziej chciałby dostać:
html, jeśli nie będzie to json, jeśli nie da rady to xml
Czyli im większa wartość parametru tym bardziej zależy mi na danym typie.
A co z:
Accept: audio/*, audio/basic
Oznacza to: daj mi proszę audio/basic, a jeśli nie dasz rady to zadowolę się dowolnym podtypem typu audio.
Content Negotiation
Nie każdy User agent (np.: przeglądarka) musi potrafić wyświetlać każdy typ danych. Negocjacja treści (Content Negotiation) polega na dobraniu najkorzystniejszej reprezentacji danych dla klienta. Możemy umieścić po stronie serwera jakiś kod, który np.: na podstawie danych z requestu (Accept, Accept-Language itp.) spróbuje dobrać najodpowiedniejszą reprezentację danych dla użytkownika.
Spójrzmy jaki nagłówek Accept jest ustawiony, gdy klientem jest przeglądarka.
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,/;q=0.8,application/signed-exchange;v=b3;q=0.9
Implementacja takiego algorytmu może być kłopotliwa. Różne przeglądarki różnie traktują wartości tego nagłówka. Np.: nadają różną wartość dla parametru jakości typu dowolnego tj.: */*. Twitter nie używa nagłówka Accept do Content Negotiation. Możesz o tym wszystkim więcej przeczytać tu.
A o innych sposobach negocjacji kontentu tu.
Host
Jeden obraz więcej niż tysiąc słów:
Kilka zdań odnośnie nagłówka Host ze specyfikacji. Jest to obowiązkowy nagłówek w HTTP 1.1. Gdy spróbujesz w Burpie go usunąć z requesta, to serwer zwróci Bad Request (400). Nagłówek Host określa host i port do którego wysyłasz request. Będzie on zignorowany jedynie wtedy, gdy podasz bezwzględną ścieżkę do zasobu (absolute-form), tzn.:
GET http://www.example.org/pub/WWW/TheProject.html HTTP/1.1 Host: example.org
Nie będę się mądrzyć o aspektach bezpieczeństwa związanych z nagłówkiem Host. Fajnie jest to opisane tu i tu i tu i w książce Sekuraka.
Accept-Encoding & Content-Encoding
Serwer na podstawie otrzymanego od klienta nagłówka Accept-Encoding wie co powinien/może ustawić w nagłówku Content-Encoding response’a. Content-Encoding jest nagłówkiem end-to-end, czyli nagłówkiem, który dotrze do celu nawet jeśli po drodze jest serwer proxy. Taki nagłówek musi zostać re-transmitowany do docelowego serwera. O nagłówkach end-to-end oraz hop-by-hop pewnie w następnej części.
Wracając do Accept-Encoding. A chodzi tu o wartość, która spowoduje kompresję (lub nie) tego co niesie w sobie response w odpowiedzi na żądanie. Np.:
Accept-Encoding: compress, gzip Accept-Encoding: * Accept-Encoding: compress;q=0.5, gzip;q=1.0 Accept-Encoding: gzip;q=1.0, identity; q=0.5, *;q=0
Brak nagłówka Accept-Encoding sugeruje, że klientowi jest wszystko jedno co dostanie. Dzięki temu, że serwer może skompresować ciało response’a, zyskamy na czasie przepychając skompresowane dane przez sieć. Jest za to pewien narzut kompresji i dekompresji. Możesz zrobić test i porównać czasy i rozmiar danych klikając Developer Toolsach czy w Postmanie. Popatrzyłem w lustro i pomyślałem właśnie co ja robię. Porównuję czasy odpowiedzi HTTP przy kompresji i jej braku w piątek wieczorem. Dlaczego ja nie jestem jeszcze pijany, tak jak lubię w ten dzień o tej porze.
Persistent connection – rfc2616#page-44
W ramach jednego połączenia TCP (którego stworzenie, nawiązanie jest kosztowne) możemy wielokrotnie skomunikować klienta i serwer (wysyłać request<->response). Połączenie trwałe jest by default w HTTP 1.1. Chodzi tu o to, że zarówno klient, jak i serwer mogą dać sygnał kończący połączenie między nimi. Jeśli serwer powie, stop, koniec, już nie gadamy (poprzez wysłanie w responsie Conection: close), to klient, według specyfikacji HTTP nie może wysłać już requestu tym połączeniem.
Serwer podobnie – może przypuszczać, że klient chce utrzymać z nim połączenie dopóki klient nie wyśle nagłówka:
Connection: close
Można by jeszcze wspomnieć o nagłówkach end-to-end i hop-by-hop i jak ma się do tego nagłówek Connection, ale na razie nie zawracajmy sobie głowy tym. Jakbyś nie mógł wytrzymać napięcia to wpisz w Google hop-by-hop http headers, end-to-end http headers. Albo zajrzyj tu.
Anyway. Co jeśli klient wyśle request, a potem cisza? Połączenie otwarte, a nic się nie dzieje. User przestał klikać, nie latają żadne requesty. W takim przypadku serwer (klient także może to zrobić) może definiować czas (a nawet liczbę requestów, choć to deprecated) po którym połączenie zostanie zamknięte. Mowa tu o nagłówku Keep-Alive i jego parametrach. Więcej przeczytasz tu.
Jeśli serwer zdecyduje o zakończeniu połączenia to również wysyła ten nagłówek.
Rekomendowanym (SHOULD) zachowaniem jest również utrzymanie połączenia nawet jeśli serwer zwróci błędną odpowiedź. Słowa musi/może/powinien mają w RFC swoje objaśnienie. W RFC spotkasz te słowa napisane drukowanymi literami np.: MUST, SHOULD. Przeczytać możesz o tym tutaj.
A drugiej części poczytamy o metodach HTTP, o typach dostępnych nagłówków i coś tam jeszcze może wpadnie.