Пару дней назад я опубликовал этот твит:
Учитывая то, насколько много я получил комментов и ответов, я решил разобрать это подробнее. Правда ли это валидный URL? Во что он парсится? Что вообще такое URL?
curl
Начнем с curl
. Он разбирает эту строку, как и должен — как валидный URL. Чтобы было понятнее, я раскрасил его составные части:
Черная часть http
— схема URL, указывает на протокол HTTP. Строка ://
разделяет схему и authority part (все, что до пути).
Красная часть http
— имя пользователя; за ним следует :
(двоеточие)
Зеленая часть //http://
— пароль; отделен знаком @
.
Синяя часть http:
— имя хоста, включая двоеточие на конце. После этого двоеточия должен идти номер порта, но если его нет, то curl
подставляет порт по умолчанию; браузеры поступают аналогично. Порт по умолчанию для HTTP имеет номер 80.
Сиреневая часть //http://
— путь. В пути вполне могут встретиться и несколько слешей подряд, и двоеточие. После пути стоит ?
(знак вопроса).
Оранжевая часть http://
— query. Все, что между ?
и #
.
Бирюзовая часть http://
— fragment, он же anchor, он же якорь, как его только не называют. Все, что справа от #
.
Можете сами попробовать запросить этот URL с помощью curl
:
curl "http://http://http://@http://http://?http://#http://" --resolve http:80:127.0.0.1
Парсер URL в curl
Парсеру URL в curl
уже пара десятков лет. Один из основных принципов его доработки — не ломать существующие скрипты и приложения. Поэтому даже если его поведение где-то расходится со стандартами, оно, вероятно, останется таким навсегда.
В самом начале разработки, парсер в curl
проглатывал очень много вольностей в URL. С годами он становился все более строгим, но следы изначального поведения еще видны тут и там. Кроме того, мы были вынуждены дорабатывать парсер, чтобы он разбирал URL из реального мира, даже если они не соответствуют никаким стандартам. Если что-то работает в браузерах и других приложениях, это должно работать и в curl
.
Стандарты URL
Как я уже писал ранее, у понятия URL на самом деле нет четкого определения.
Есть определение URI — не URL! — в RFC 3986. Есть спецификация WHATWG, которой следуют браузеры (или пытаются). А еще есть множество конкретных реализаций разной степени строгости, которые совершенно не обязательно следуют хоть какому-то из этих двух определений.
Если приглядеться повнимательней, то вряд ли хоть какие-то две реализации будут полностью соответствовать друг другу.
Поэтому если попытаться использовать наш безумный URL где-то в реальном мире, нельзя сказать точно, примут его или нет. Твиттер, к примеру, не понял, что это URL. И даже если он окажется валидным для них, они, скорее всего, распарсят его по-другому.
Python и urllib
April King протестировала наш безумный URL в питоновской urllib
. И хотя он и разобрался успешно, но совершенно по-другому:
ParseResult(scheme='http',
netloc='http:',
path='//http://@http://http://',
params='',
query='http://',
fragment='http://')
Судя по ответам на мой твит, такой интерпретации придерживаются еще несколько парсеров. Вполне возможно, у них на самом деле один и тот же парсер под капотом.
JavaScript
Meduz показал мне, как этот URL разбирает JavaScript, и это оказалось очень похоже на наш результат с Python:
Firefox и Chrome
Я дописал 127.0.0.1 http
в /etc/hosts
, и скопипастил наш безумный URL в адресную строку Firefox. Браузер переписал его вот так:
http://http//http://@http://http://?http://#http://
Второе слева двоеточие съелось, все остальное осталось как прежде. Но, тем не менее, Firefox воспринял это как валидный URL, и открыл страницу с моего локального сервера.
Chrome повел себя точно так же.
RFC 3986
В комментах кто-то заметил, что слэши в authority, не закодированные urlencode, не соответствуют RFC 3986. Секция 3.2 этого документа гласит:
The authority component is preceded by a double slash ("//") and is terminated by the next slash ("/"), question mark ("?"), or number sign ("#") character, or by the end of the URI.
Это значит, что в нашем пароле //http://
слеши в начале и конце должны быть заменены на %2f
. То есть, получается, URL не валидный?
Update (от автора): похоже, что он все еще считается валидным, просто должен быть разобран иначе, чем это делает curl
.
HTTPS
Этот URL может быть переписан с использованием HTTPS:
https://https://https://@https://https://?https://#https://
Но я не использовал эту форму по нескольким причинам:
Это на семь символов длиннее.
Это сложнее протестировать локально, без возни с сертификатами.
Комментарии (18)
vabka
09.09.2022 16:44+2.NET тоже считает такой url валидным, но обрабатывает как-то странно:
var uri = new Uri(@"http://http://http://@http://http://?http://#http://"); Console.WriteLine(uri.Scheme); // http // username и password вытащить нельзя, или я не нашёл как Console.WriteLine(uri.Host); // http Console.WriteLine(uri.Port); // 80 - это default port для http, по тому так и вышло Console.WriteLine(uri.AbsolutePath); // //http://@http://http:// Console.WriteLine(uri.PathAndQuery); // //http://@http://http://?http:// Console.WriteLine(uri.Query); // ?http:// Console.WriteLine(uri.Fragment); // #http://
Evengard
09.09.2022 17:02+2Что .NET, что упомянутый urllib, что Javascript парсят урлы примерно одинаково, не воспринимая понятие логина и пароля в URL как класс и соотв. интерпретируя их как домен и кусок пути.
AlexYasinovsky
10.09.2022 12:43+1uri.UserInfo
https://docs.microsoft.com/ru-ru/dotnet/api/system.uri.userinfo?view=net-6.0#system-uri-userinfo
thewizardplusplus
09.09.2022 21:14+3Go парсит оригинальный URL так же, как Python и JavaScript. Код:
Hidden text
package main import ( "encoding/json" "fmt" "log" "net/url" ) func main() { url, err := url.Parse("http://http://http://@http://http://?http://#http://") if err != nil { log.Fatal(err) } // конвертирую в JSON для красивого вывода urlBytes, err := json.MarshalIndent(url, "", " ") if err != nil { log.Fatal(err) } fmt.Println(string(urlBytes)) }
Вывод:
Hidden text
{ "Scheme": "http", "Opaque": "", "User": null, "Host": "http:", "Path": "//http://@http://http://", "RawPath": "", "OmitHost": false, "ForceQuery": false, "RawQuery": "http://", "Fragment": "http://", "RawFragment": "" }
Online: https://go.dev/play/p/q8qiLbkPriz
Если же, как указал автор, экранировать слэши в пароле, URL парсится так, как ожидалось изначально. Код:
Hidden text
package main import ( "encoding/json" "fmt" "log" "net/url" ) func main() { url, err := url.Parse("http://http:%2f%2fhttp:%2f%2f@http://http://?http://#http://") if err != nil { log.Fatal(err) } if url.User == nil { log.Fatal("не могу распарсить данные пользователя") } // конвертирую в JSON для красивого вывода urlBytes, err := json.MarshalIndent(url, "", " ") if err != nil { log.Fatal(err) } fmt.Println(string(urlBytes)) // структура url.Userinfo содержит только приватные поля, поэтому в JSON не попадёт; вывожу её отдельно fmt.Printf("Username: %q\n", url.User.Username()) if password, ok := url.User.Password(); ok { fmt.Printf("Password: %q\n", password) } }
Вывод:
Hidden text
{ "Scheme": "http", "Opaque": "", "User": {}, "Host": "http:", "Path": "//http://", "RawPath": "", "OmitHost": false, "ForceQuery": false, "RawQuery": "http://", "Fragment": "http://", "RawFragment": "" } Username: "http" Password: "//http://"
v1000
09.09.2022 22:50+7В свое время Windows 98 крэшилась при попытке открыть c:/con/con. Но это локально. Потом кто-то догадался на веб страничке добавить этот путь в качестве адреса для картинки. Открываешь интернет сайт и система крэшится.
CaptainFlint
11.09.2022 20:32В гораздо более современный год обнаружилось, что обращение к C:\$MFT\foo на NTFS приводит к зависанию или крэшу. Правда, через браузеры это эксплуатировать уже было сложнее, так как обращения к локальным ресурсам с интернет-страницы во всех нормальных браузерах уже были заблокированы, и подверженным проблеме оставался один IE.
Myateznik
09.09.2022 23:27+7URL это URI. В URI обязательными частями являются только scheme и path, части authority, query, fragment опциональные. Если опциональные части не проходят по формату - они игнорируются, если точнее интерпретируются как часть обязательной части (при прохождение по требованиям этой части). Тоже самое происходит и в формате опциональной части authority (тут обязательный только host).
Получается данный URI разбивается так:
host ┌─┴┐ http://http://http://@http://http://?http://#http:// └─┬┘ └──┬──┘└──────────┬───────────┘└───┬──┘└───┬──┘ scheme authority path query fragment
В итоге поведение Python, JavaScript, Go, .NET и браузеров является правильным и следует RFC (браузеры в дополнение ещё убирают лишние части, собственно по этому "съелось" двоеточие у не обязательного порта), поведение curl не соответствует RFC в полной мере.
osmanpasha
10.09.2022 07:23+1Странно, что двоеточие без указания порта является валидным. Было бы логично считать опциональным всю часть":8080", а двоеточие без порта - ошибкой
domix32
10.09.2022 09:34-1Да господи, цветов нафигачили, скобочек понарисовали, а было достаточно записать его шаблонном виде без выёживаний
http://user:password@host:/?query#fragment
kpmy
10.09.2022 13:29EBNF-схема есть, правила построения парсеров есть, ну да, глазу человека неприятно это видеть, но компу-то что, даже странно, откуда собственно проблема?
iliazeus Автор
Для тех, кто хочет потестить где-то еще:
http://http://http://@http://http://?http://#http://
(эй, Хабр, это валидный URL?)