Każda część adresu URL i to, co może zawierać
Adres URL to jeden ciąg znaków, który mówi, jakim protokołem się posłużyć, z którą maszyną rozmawiać i o co poprosić. Poprawne odczytanie go sprowadza się głównie do wiedzy, gdzie kończy się każda część, ponieważ separatorami są pojedyncze znaki, a liczy się pierwszy napotkany.
https://alex:s3cret@www.example.co.uk:8443/docs/intro?lang=en&page=2#notes
│ │ │ │ │ │ └─ fragment
│ │ │ │ │ └──────────────── query
│ │ │ │ └──────────────────────────── path
│ │ │ └──────────────────────────────── port
│ │ └────────────────────────────────────────────────── host
│ └────────────────────────────────────────────────────────────── userinfo
└────────────────────────────────────────────────────────────────────── scheme
Poszczególne części
| Część | Ograniczona przez | Do czego służy | Trafia na serwer |
|---|---|---|---|
| Schemat | kończy się na pierwszym : | Który protokół i które reguły obowiązują dla reszty | Decyduje o połączeniu |
| Userinfo | po //, kończy się na @ | Dane logowania, od dawna wycofane dla http i https | Tylko jeśli klient zdecyduje się je wysłać |
| Host | kończy się na :, /, ? lub # | Nazwa domeny albo adres IP, z którym trzeba się połączyć | Tak, w nagłówku Host |
| Port | po :, kończy się na /, ? lub # | Port TCP, domyślnie 80 dla http i 443 dla https | Używany przy nawiązywaniu połączenia |
| Ścieżka | zaczyna się od /, kończy się na ? lub # | Który zasób na tym hoście | Tak, w wierszu żądania |
| Zapytanie | zaczyna się od ?, kończy się na # | Parametry dla tego zasobu | Tak, w wierszu żądania |
| Fragment | zaczyna się od #, biegnie do końca | Miejsce wewnątrz zasobu | Nie |
Host i port razem tworzą authority. Fragment jest tu wyjątkiem: zostaje usunięty, zanim żądanie zostanie zbudowane, więc serwer nigdy go nie widzi. Przeglądarki używają go do kotwic i do routingu po stronie klienta, a niektóre procesy uwierzytelniania celowo zwracają tokeny we fragmencie, żeby te tokeny nigdy nie pojawiły się w logu serwera. Fragment i tak trafia do historii przeglądarki, więc nie jest prywatny, jest tylko niewysyłany.
Znaki zarezerwowane i niezarezerwowane
Kodowanie procentowe zapisuje bajt jako % plus dwie cyfry szesnastkowe, korzystając z bajtów UTF-8 danego znaku. é staje się %C3%A9, dwa bajty i dwie sekwencje ucieczki.
Cztery znaki są niezarezerwowane obok liter i cyfr i nigdzie nie wymagają kodowania: - . _ ~. Cała reszta jest albo zarezerwowana, czyli pełni gdzieś w adresie URL rolę strukturalną, albo musi zostać zakodowana. Zbiór zarezerwowany to : / ? # [ ] @ oraz ! $ & ' ( ) * + , ; =.
Słowo "zarezerwowany" nie oznacza "zawsze zabroniony". Oznacza, że znak jest separatorem w którejś części i musi zostać zakodowany tylko tam, gdzie zostałby odczytany jako separator.
| Znak | W segmencie ścieżki | W wartości zapytania |
|---|---|---|
| Spacja | %20, zawsze | %20 albo + |
/ | %2F, inaczej rozdziela segment | Dozwolony w tej postaci |
? | %3F, inaczej ścieżka się tam kończy | Dozwolony w tej postaci |
# | %23, zawsze | %23, zawsze |
& i = | Dozwolone w tej postaci | %26 i %3D, inaczej para się rozpada |
+ | Dozwolony i oznacza plus | %2B, inaczej może zostać zdekodowany jako spacja |
Praktyczna reguła brzmi więc tak: koduj ten znak, który zakończyłby część, w której właśnie jesteś. Spacja w ścieżce musi być zapisana jako %20, bo surowa spacja kończy adres URL w większości parserów, natomiast spacja w wartości zapytania może być zapisana jako %20 albo starszym +. Koder URL to pewny sposób, żeby to zrobić, ponieważ zakodowanie całego adresu i zakodowanie pojedynczego komponentu to dwie różne operacje, a prawie zawsze chodzi o tę drugą.
Ciąg zapytania to tylko konwencja
Specyfikacja mówi wyłącznie tyle, że zapytanie to wszystko pomiędzy ? a #, złożone z dozwolonych znaków. Postać key=value&key=value pochodzi z kodowania formularzy HTML, a nie ze standardu URI. Nic nie stoi na przeszkodzie, żeby serwer parsował ?a:1;b:2 tak, jak mu się podoba.
Ponieważ ta postać jest tylko konwencją, powtórzone klucze nie mają zdefiniowanego znaczenia i każdy stos technologiczny wybrał własną odpowiedź:
Zachowanie dla ?id=1&id=2 | Gdzie |
|---|---|
| Obie wartości, jako lista | Python parse_qs, Node querystring, Express |
| Tylko pierwsza wartość | Go Query().Get, Java getParameter |
| Tylko ostatnia wartość | PHP, Rails |
Sklejone w 1,2 | kolekcje żądań w ASP.NET |
Tablice dziedziczą ten sam problem. ids=1&ids=2, ids[]=1&ids[]=2, ids[0]=1&ids[1]=2 oraz ids=1,2 są wszystkie szeroko używane i tylko serwer decyduje, którą postać rozumie. Zapis z nawiasami trzeba zakodować, żeby był ściśle zgodny z normą (ids%5B%5D=1), choć większość serwerów akceptuje surowe nawiasy. Wybierz tę postać, którą dokumentuje strona odbierająca, a potem trzymaj się jej konsekwentnie.
Znak plus zasługuje na osobne ostrzeżenie. W danych application/x-www-form-urlencoded, czyli w treści żądania zakodowanej jak formularz, + oznacza spację. W ścieżce URL oznacza dosłowny plus. W ciągu zapytania zależy to od parsera, a większość frameworków webowych stosuje tam dekodowanie formularzowe, więc + po cichu zamienia się w spację. Każda wartość, która może w uzasadniony sposób zawierać plus, na przykład standardowe wyjście Base64 albo numer telefonu, musi zostać wysłana jako %2B.
Odwołania względne
Odwołanie względne jest rozwiązywane wobec adresu bazowego przez zastąpienie wszystkiego, co znajduje się po ostatnim / w tym adresie. Ten ostatni ukośnik to cała reguła i właśnie dlatego ukośnik na końcu ma znaczenie.
| Adres bazowy | Odwołanie | Wynik |
|---|---|---|
https://ex.com/docs/intro | guide | https://ex.com/docs/guide |
https://ex.com/docs/intro/ | guide | https://ex.com/docs/intro/guide |
https://ex.com/docs/intro | /guide | https://ex.com/guide |
https://ex.com/docs/intro/ | ../guide | https://ex.com/docs/guide |
https://ex.com/docs/intro?a=1 | ?b=2 | https://ex.com/docs/intro?b=2 |
https://ex.com/docs/intro?a=1 | #top | https://ex.com/docs/intro?a=1#top |
https://ex.com/docs/intro | //cdn.ex.com/x.js | https://cdn.ex.com/x.js |
Ostatni wiersz to odwołanie względne wobec schematu: dwa wiodące ukośniki zachowują schemat adresu bazowego i zastępują authority. Odwołanie zaczynające się od ? zachowuje ścieżkę i porzuca stare zapytanie, a takie, które zaczyna się od #, zachowuje jedno i drugie.
Kiedy dwa adresy URL to ten sam zasób
Schemat i host nie rozróżniają wielkości liter, więc HTTPS://Example.COM i https://example.com to jeden adres. Wszystko po hoście jest, z punktu widzenia standardu, wrażliwe na wielkość liter, nawet jeśli konkretny serwer postanawia ją ignorować.
Te pary są równoważne:
https://example.com:443/aihttps://example.com/a, ponieważ port jest domyślnyhttps://example.comihttps://example.com/, ponieważ pusta ścieżka oznacza katalog główny/a/%7Euseri/a/~user, ponieważ~jest niezarezerwowany i zakodowanie go niczego nie zmienia
Te pary równoważne nie są:
/docsi/docs/, które są różnymi zasobami, choć większość serwerów przekierowuje jeden na drugi/p?a=1&b=2i/p?b=2&a=1, ponieważ kolejność parametrów jest częścią ciągu znaków/index.htmli/, chyba że serwer mówi inaczej
Pamięci podręczne i sieci CDN kluczują po dokładnych bajtach, więc dodany parametr śledzący albo przestawiona kolejność zapytania rozbija jeden obiekt w pamięci podręcznej na dwa i o połowę obniża trafienia. Wyszukiwarki traktują warianty jako zduplikowane strony, dopóki odnośnik kanoniczny nie wskaże jednej wybranej postaci. Rozwiązaniem jest wybrać jedną postać, przekierować na nią resztę kodem 301 i usuwać parametry, które nie zmieniają odpowiedzi.
Długość, logi i prywatność
W specyfikacji nie ma limitu długości, ale limity istnieją wszędzie indziej. Typowe domyślne ustawienia serwerów ograniczają cały wiersz żądania do około 8 KB, niektóre serwery proxy i urządzenia sieciowe do 4 KB, a starsze klienty do mniej więcej 2 KB. Wszystko, co ma przetrwać programy pocztowe, kody QR i skracacze linków, jest najbezpieczniejsze poniżej mniej więcej 2000 znaków.
Mocniejszym argumentem za krótkimi adresami URL jest to, że ciąg zapytania nie jest prywatny. Trafia do logów dostępu, jest przekazywany podmiotom trzecim w nagłówku Referer, zostaje w historii przeglądarki, zapisuje się razem z zakładką i jest kopiowany za każdym razem, gdy ktoś udostępnia odnośnik. Tokeny sesji, kody resetu hasła, klucze API i dane osobowe nie mają tam więc czego szukać. Dane, które są duże albo wrażliwe, idą do treści żądania, która nie ma praktycznego limitu rozmiaru i domyślnie nie jest logowana.
Czytanie odnośnika przed kliknięciem
Authority zaczyna się po :// i kończy na pierwszym napotkanym /, ? lub #. Odczytaj ten odcinek, a potem odczytaj jego dwie albo trzy ostatnie etykiety. To jest prawdziwy host i nic, co stoi po jego lewej stronie, tego nie zmienia.
https://www.paypal.com@198.51.100.7/secure/login
^^^^^^^^^^^^^^ ^^^^^^^^^^^^
userinfo real host
Typowe przebrania, które warto rozpoznawać:
- Tekst przed
@to userinfo, nigdy host, i może to być dowolna nazwa marki. paypal.com.secure-login.exampleto subdomenasecure-login.example. Etykiety czyta się od prawej do lewej.https://short.example/https://www.bank.com/loginumieszcza cały przekonująco wyglądający adres URL w ścieżce.- Host zaczynający się od
xn--to Punycode, czyli postać ASCII umiędzynarodowionej nazwy domeny wytworzona przez IDNA.münchen.deto w transmisjixn--mnchen-3ya.de, co jest zupełnie legalne, ale ten sam mechanizm umożliwia homografy: cyrylickieа(U+0430) wygląda identycznie jak łacińskiea, a domena, która go używa, koduje się do czegoś w rodzajuxn--pple-43d.com. Przeglądarki pokazują postać Punycode, gdy etykieta miesza pisma, choć reguły bywają różne i nie dają gwarancji. - Zakodowane separatory takie jak
%2Fczy%40wewnątrz hosta to celowa próba zmylenia parsera.
Wklejenie odnośnika do parsera URL rozstrzyga sprawę w jednym kroku, bo prawdziwy parser stosuje tę samą regułę pierwszego separatora, którą zastosuje przeglądarka, i pokazuje host osobno. Do sprawdzania wielu odnośników naraz tester wyrażeń regularnych jest szybkim sposobem na potwierdzenie, które z nich rzeczywiście mają oczekiwane authority.