URI

URI — унифицированный идентификатор ресурса. То же: Uniform (прежде Universal) Resource Identifier.

URI бывает двух-трёх видов:

Пример URL: http://karmanov.su/URI.html. Пример URN: urn:isbn:1937785536. В одном случае мы можем получить требуемый ресурс моментально (страница данного сайта), в другом случае моментально этого не получится сделать. ISBN — это международная нумерация книг. urn:isbn:1937785536 тоже обозначает конкретный ресурс (книга Джо Армстронга), однако в этом обозначении не сказано, как получить ресурс. Мы можем воспользоваться поисковиком, чтобы найти интернет-магазин, торгующий данной книгой. Скорее всего, будет найдено несколько источников, но может быть и так, что книги уже нет в продаже.

URL указывает на электронный ресурс, URN может указывать на материально существующую вещь. На одну и ту же вещь могут указывать несколько идентификаторов. Например, мы можем пронумеровать определённым образом книги в домашней библиотеке — и тогда идентификатор будет показывать на конкретную книгу, стоящую на полке. У этой же книги, скорее всего, есть и ISBN — как и у других книг, вышедших этим же тиражом.

Нет и не может быть в принципе какого-то единого алгоритма превращения URI в наличный ресурс. URI — расширяемый способ идентификации ресурсов. Есть несколько схем идентификации внутри URI, многие будут созданы. Нет ничего страшного в том, чтобы придумать свои схемы, свои протоколы.

Если мы строим свою Систему, используя URI, мы легко можем реализовать универсальное обращение к ресурсам. Часто в качестве аргументов функций мы передаём имя файла, например /home/user/report-20241014.csv. Это удобно до тех пор, пока мы получаем файлы локально. Пройдёт время, и возможно, нам понадобится больше гибкости. Допустим, на одной ноде мы и дальше сможем получать файлы локально, но на другой ноде — нет.

Тогда удобным станет обрабатывать не имена файлов, а URL. В одном случае будет обрабатываться URL file:///home/user/report-20241014.csv. В другом: http://localhost/report-20241014.csv. Функция получает этот URL и смотрит, что это за зверь такой, по какому протоколу надо получить этот ресурс (file или http). В зависимости от протокола она получает нужный ресурс и дальше с ним что-то делает.

Пройдёт время и, может быть, возникнет такая ситуация, что мы точно знаем, что где-то на компьютере есть файл report-20241014.csv, но где точно он находится, мы не знаем. Разные бывают ситуации, и не всё зависит от нас. Мы тогда можем придумать какой-нибудь свой специальный URN для таких случаев, и тогда функция (помимо других вариантов) будет принимать ещё и URI вроде urn:lostfile:report-20241014.csv. Проанализировав полученный URI, функция поймёт, что ей придётся попотеть и найти нужный файл на компьютере самостоятельно. Для этого она, например, запустит команду find.

Почему мы начали свой URI с urn:? Сложилась такая практика, что когда URI не указывает на конкретное место, когда он не связан с каким-то конкретным протоколом или методом доступа, тогда используется префикс urn:.

Анализ URI в Эрланге

Для работы с URI есть специальный модуль uri_string. Чтобы разложить URI на части, можно воспользоваться функцией parse/1 из этого модуля, которая в качестве аргумента принимает строку, а возвращает карту, содержащую части URI.

1> uri_string:parse("http://karmanov.su/URI.html").
#{scheme => "http",path => "/URI.html",host => "karmanov.su"}
2> uri_string:parse("ftp://ftp.is.co.za/rfc/rfc1808.txt").
#{scheme => "ftp",path => "/rfc/rfc1808.txt",
  host => "ftp.is.co.za"}
3> uri_string:parse("mailto:alex@karmanov.su").
#{scheme => "mailto",path => "alex@karmanov.su"}
4> uri_string:parse("telnet://192.0.2.16:80/").
#{port => 80,scheme => "telnet",path => "/",
  host => "192.0.2.16"}
5> uri_string:parse("tel:+1-816-555-1212").
#{scheme => "tel",path => "+1-816-555-1212"}

Как видим, во всех случаях были выделены части scheme и path. Остальное — факультативно. Всего может быть выделено в URI семь частей:

Попробуем получить полный набор этих “запчастей”:

10> uri_string:parse("foo://alex@localhost:8080/some/util?name=
    pushkin#balda").
#{port => 8080,scheme => "foo",path => "/some/util",
  host => "localhost",fragment => "balda",
  query => "name=pushkin",userinfo => "alex"}

О чём говорит этот URI и карта, которую мы получили? На локальном компьютере (“localhost”) запущен некий сервер, который готов общаться с нами по некоторому протоколу (“foo”). Сервер прослушивает определённый порт (8080) и ожидает лишь определённых пользователей (“alex”). В ведении сервера есть некий каталог, а в нём некоторая утилита (“/some/util”). Эта утилита принимает аргументы, и если ей передать аргумент “name=pushkin”, то она, видимо, отдаст страницу, содержащую ряд пушкинских стихотворений. Там, очевидно, есть “Сказка о попе и о работнике его Балде”, раз указан фрагмент “balda”. Мы желаем, чтобы наша программа-обозреватель открыла нам сборник стихотворений именно на этом месте.

Тут, конечно, есть некоторые условности. Для аутентификации на сервере совсем не обязательно в URI задавать имя пользователя, можно использовать другие механизмы. Та часть, которая идёт после имени хоста и порта (/some/util?name=pushkin) не является безусловным доказательством, что на сервере и правда есть каталог some, а в нём программа util. Это внутреннее дело сервера — как интерпретировать данную строку. Вместо /some/util?name=pushkin, в принципе, может быть и /someutilnamepushkin. Если сервер сможет это разобрать, значит, он молодец.

Каждый URI это цепочка символов. В URI могут использоваться лишь ASCII-символы, и то далеко не все. Функция parse, встречая такие символы, выдаёт ошибку.

1> uri_string:parse("foo://бегемот/").
{error,invalid_uri,":"}
2> uri_string:parse("foo://begemot/a`b").
{error,invalid_uri,":"}

Составление URI

Внутри системы целесообразно хранить и передавать URI не в сыром виде (строка), а в уже разложенном виде — в виде терма карты. Это удобно, особенно если мы собираемся анализировать URI.

Когда URI попадает в Систему, он превращается в карту, а когда выходит обратно, то из карты превращается назад в строку. Это может произойти, например, в случае, когда мы желаем использовать какую-то внешнюю команду, например curl, для получения ресурса.

В uri_string есть функция, обратная parse, это recompose.

1> Uri1 = "foo://alex@localhost:8080/some/util?name=pushkin#balda".
"foo://alex@localhost:8080/some/util?name=pushkin#balda"
2> Uri_map = uri_string:parse(Uri1).
#{port => 8080,scheme => "foo",path => "/some/util",
  host => "localhost",fragment => "balda",
  query => "name=pushkin",userinfo => "alex"}
3> Uri2 = uri_string:recompose(Uri_map).
"foo://alex@localhost:8080/some/util?name=pushkin#balda"
4> Uri1 = Uri2. 
"foo://alex@localhost:8080/some/util?name=pushkin#balda"

Разложив, а потом снова сложив, мы получили, как видно, ту же самую строку URI.

Нормализация URI

Также в модуле uri_string есть удобная функция normalize, которая позволяет нормализовать URI. Эту функцию, кстати, можно использовать вместо recompose/1.

В названии хоста не имеет значения, в каком регистре написаны буквы. В пути, однако, это имеет значение. Нормализуясь, имя хоста состоит только из букв в нижнем регистре.

1> uri_string:normalize(#{scheme => "http", host => "Localhost",
    path => "/Index.html"}).
"http://localhost/Index.html"

Неправильные символы получают правильное отображение.

2> uri_string:normalize(#{scheme => "http", host => "локалхост",
    path => "/index.html"}).
"http://%D0%BB%D0%BE%D0%BA%D0%B0%D0%BB%D1%85%D0%BE%D1%81%D1%82/
    index.html"

Запутанные пути распутываются.

3> uri_string:normalize(#{scheme => "http", host => "localhost",
    path => "/a/b/c/./../../d"}).
"http://localhost/a/d"

Порт, который является дефолтным, будет опущен.

4> uri_string:normalize(#{scheme => "http", host => "localhost",
    path => "/index.html", port => 80}).
"http://localhost/index.html"

Составление запросов

Через URI может передаваться аргумент, который будет воспринят сервером и обработан соответствующим образом. Для правильной генерации этой части URI (query) мы можем воспользоваться функцией uri_string:compose_query. В качестве аргумента она принимает список кортежей, каждый из которых состоит из двух элементов: ключ и значение.

1> L = [{"Автор","Державин"},{"Произведение","Снигирь"}].
[{[1040,1074,1090,1086,1088],
  [1044,1077,1088,1078,1072,1074,1080,1085]},
 {[1055,1088,1086,1080,1079,1074,1077,1076,1077,1085,1080,
   1077],
  [1057,1085,1080,1075,1080,1088,1100]}]
2> uri_string:compose_query(L).
"%D0%90%D0%B2%D1%82%D0%BE%D1%80=%D0%94%D0%B5%D1%80%D0%B6%D0%B0
    %D0%B2%D0%B8%D0%BD&%D0%9F%D1%80%D0%BE%D0%B8%D0%B7%D0%B2%D0
    %B5%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5=%D0%A1%D0%BD%D0%B8%D0%B3
    %D0%B8%D1%80%D1%8C"

Разложение запросов

Для функции compose_query есть обратная — dissect_query, которая принимает query (часть URI — запрос) и возвращает список кортежей “ключ-значение”.

$ erl +pc unicode
1> uri_string:dissect_query("%D0%90%D0%B2%D1%82%D0%BE%D1%80=%D0
%94%D0%B5%D1%80%D0%B6%D0%B0%D0%B2%D0%B8%D0%BD&%D0%9F%D1%80%D0%BE
%D0%B8%D0%B7%D0%B2%D0%B5%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5=%D0%A1%D0
%BD%D0%B8%D0%B3%D0%B8%D1%80%D1%8C").
[{"Автор","Державин"},{"Произведение","Снигирь"}]

Кодирование и декодирование частей URI

Если стоит задача кодировать в приемлемые символы какую-то часть URI (путь, запросы), для этого может пригодиться функция quote.

1> uri_string:quote("Поэты/Лермонтов").
"%D0%9F%D0%BE%D1%8D%D1%82%D1%8B%2F%D0%9B%D0%B5%D1%80%D0%BC%D0
    %BE%D0%BD%D1%82%D0%BE%D0%B2"
2> uri_string:quote("Поэты/Лермонтов", "/").
"%D0%9F%D0%BE%D1%8D%D1%82%D1%8B/%D0%9B%D0%B5%D1%80%D0%BC%D0%BE
    %D0%BD%D1%82%D0%BE%D0%B2"

В первом случае кодируются все неправильные символы, включая слэш. Если эта строка — запрос (аргумент для программы на сервере), то это нормально. Но возможно, что “Поэты/Лермонтов” относится к пути, поэтому слэш надо сохранить. В этом случае можно использовать quote/2, где второй аргумент — список символов, которые не надо кодировать.

Чтобы декодировать строку — часть URI, есть обратная функция — unquote/1.

3> uri_string:unquote("%D0%9F%D0%BE%D1%8D%D1%82%D1%8B%2F%D0%9B
    %D0%B5%D1%80%D0%BC%D0%BE%D0%BD%D1%82%D0%BE%D0%B2").
"Поэты/Лермонтов"
4> uri_string:unquote("%D0%9F%D0%BE%D1%8D%D1%82%D1%8B/%D0%9B%D0
    %B5%D1%80%D0%BC%D0%BE%D0%BD%D1%82%D0%BE%D0%B2").
"Поэты/Лермонтов"

В какой части URI какие символы допустимы?

В разных частях URI используется разный набор символов. Например, в схеме можно использовать лишь цифры, буквы (в обоих регистрах), плюс, минус и точку. Другие символы приведут к ошибке:

1> uri_string:parse("fo_od://localhost").
{error,invalid_uri,":"}
2> uri_string:parse("fo%od://localhost").
{error,invalid_uri,":"}

Но как узнать или вспомнить, в какой части URI какие символы используются? Для этого есть удобная функция uri_string:allowed_characters/0. Она носит справочный характер, но её, конечно, можно использовать и в программах для автоматизации каких-либо процессов проверки.

1> uri_string:allowed_characters().
[{scheme,"+-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmn
    opqrstuvwxyz"},
 {userinfo,"!$%&'()*+,-.0123456789:;=ABCDEFGHIJKLMNOPQRSTUVWXYZ_
    abcdefghijklmnopqrstuvwxyz~"},
 {host,"!$&'()*+,-.0123456789:;=ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcd
    efghijklmnopqrstuvwxyz~"},
 {ipv4,".0123456789"},
 {ipv6,".0123456789:ABCDEFabcdef"},
 {regname,"!$%&'()*+,-.0123456789;=ABCDEFGHIJKLMNOPQRSTUVWXYZ_a
    bcdefghijklmnopqrstuvwxyz~"},
 {path,"!$%&'()*+,-./0123456789:;=@ABCDEFGHIJKLMNOPQRSTUVWXYZ_a
    bcdefghijklmnopqrstuvwxyz~"},
 {query,"!$%&'()*+,-./0123456789:;=?@ABCDEFGHIJKLMNOPQRSTUVWXYZ
    _abcdefghijklmnopqrstuvwxyz~"},
 {fragment,"!$%&'()*+,-./0123456789:;=?@ABCDEFGHIJKLMNOPQRSTUVW
    XYZ_abcdefghijklmnopqrstuvwxyz~"},
 {reserved,"!#$&'()*+,/:;=?@[]"},
 {unreserved,"-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijk
    lmnopqrstuvwxyz~"}]

Распутывание путей

Как уже говорилось выше, при нормализации URI запутанный путь в нём распутывается. Если у нас не стоит задача нормализовать URI, но хочется уже распутать запутанные пути, для этого есть функция resolve.

1> uri_string:resolve("/abs/ol/ute", "http://localhost/a/b/c/d?
    query").
"http://localhost/abs/ol/ute"
2> uri_string:resolve("http://nonlocal/abs/ol/ute",
    "http://localhost/a/b/c/d?query").
"http://nonlocal/abs/ol/ute"
3> uri_string:resolve("../../b/.././",
    "http://localhost/a/b/c/d?query").
"http://localhost/a/"

Второй аргумент — базовый URI. Первый — путь, который мы желаем распутать. Он может быть как абсолютным, так и относительным.


© Алексей Карманов, 2024.