Пару дней назад я опубликовал этот твит:

Учитывая то, насколько много я получил комментов и ответов, я решил разобрать это подробнее. Правда ли это валидный 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)


  1. iliazeus Автор
    09.09.2022 15:24
    +8

    Для тех, кто хочет потестить где-то еще:

    http://http://http://@http://http://?http://#http://

    (эй, Хабр, это валидный URL?)


  1. alexlifewords
    09.09.2022 16:40
    +13

    <пользователь http поменял пароль>


    1. ainu
      09.09.2022 22:54

      http://http://


  1. 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://


    1. Evengard
      09.09.2022 17:02
      +2

      Что .NET, что упомянутый urllib, что Javascript парсят урлы примерно одинаково, не воспринимая понятие логина и пароля в URL как класс и соотв. интерпретируя их как домен и кусок пути.


    1. kemsky
      09.09.2022 21:44

      А если проверить на well formed?


    1. AlexYasinovsky
      10.09.2022 12:43
      +1

      uri.UserInfo https://docs.microsoft.com/ru-ru/dotnet/api/system.uri.userinfo?view=net-6.0#system-uri-userinfo


  1. SashKotina
    09.09.2022 16:50
    +2

    На хабре даже название статьи читается как URL:

    Hidden text


  1. ris58h
    09.09.2022 17:22

    Можно ещё, path segment parameters прикрутить, наверное.


  1. thewizardplusplus
    09.09.2022 21:14
    +3

    Go парсит оригинальный 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://"
    

    Online: https://go.dev/play/p/lM5RdZw82PO


  1. v1000
    09.09.2022 22:50
    +7

    В свое время Windows 98 крэшилась при попытке открыть c:/con/con. Но это локально. Потом кто-то догадался на веб страничке добавить этот путь в качестве адреса для картинки. Открываешь интернет сайт и система крэшится.


    1. CaptainFlint
      11.09.2022 20:32

      В гораздо более современный год обнаружилось, что обращение к C:\$MFT\foo на NTFS приводит к зависанию или крэшу. Правда, через браузеры это эксплуатировать уже было сложнее, так как обращения к локальным ресурсам с интернет-страницы во всех нормальных браузерах уже были заблокированы, и подверженным проблеме оставался один IE.


  1. Myateznik
    09.09.2022 23:27
    +7

    URL это 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 в полной мере.


    1. osmanpasha
      10.09.2022 07:23
      +1

      Странно, что двоеточие без указания порта является валидным. Было бы логично считать опциональным всю часть":8080", а двоеточие без порта - ошибкой


    1. domix32
      10.09.2022 09:34
      -1

      Да господи, цветов нафигачили, скобочек понарисовали, а было достаточно записать его шаблонном виде без выёживаний

      http://user:password@host:/?query#fragment


  1. kpmy
    10.09.2022 13:29

    EBNF-схема есть, правила построения парсеров есть, ну да, глазу человека неприятно это видеть, но компу-то что, даже странно, откуда собственно проблема?


  1. habrabkin
    10.09.2022 16:28
    +2

    Админы http.com офигевают сейчас…


  1. VictorFilimonov
    11.09.2022 08:01
    -2

    это - явно не валидный урл. Такое даже человеку не понять.