From 73eb8f0e81cdeffc94307287e95737d4f938d15a Mon Sep 17 00:00:00 2001 From: Dmitry Ivanov Date: Tue, 8 Sep 2026 10:53:26 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D0=BA=D1=83?= =?UTF-8?q?=20=D0=BF=D0=B5=D1=82=D0=BB=D0=B5=D0=B2=D0=BE=D0=B3=D0=BE=20URL?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/URL.md | 45 ++++++ .../URL.os" | 46 +++++++ ...273\320\265\320\262\320\276\320\271URL.os" | 130 ++++++++++++++++++ 3 files changed, 221 insertions(+) create mode 100644 "tests/URL/\320\237\320\265\321\202\320\273\320\265\320\262\320\276\320\271URL.os" diff --git a/docs/URL.md b/docs/URL.md index 5c6633c..8079432 100644 --- a/docs/URL.md +++ b/docs/URL.md @@ -35,6 +35,7 @@ - [УстановитьКодированныеДанныеПользователя](#установитькодированныеданныепользователя) - [Хост URL](#хост-url) - [ВидХоста](#видхоста) + - [ЯвляетсяПетлевым](#являетсяпетлевым) - [КодированныйХост](#кодированныйхост) - [УстановитьКодированныйХост](#установитькодированныйхост) - [УстановитьХост](#установитьхост) @@ -539,6 +540,50 @@ ASCII, но не заменяет доменное имя на Punycode. `Строка` из модуля [`ВидыХостаURL`](ВидыХостаURL.md) или `Неопределено` без серверной части. +### ЯвляетсяПетлевым + +Проверяет, указывает ли явно заданный хост URL на сетевой интерфейс обратной петли (loopback). +Проверка выполняется без DNS-запросов и обращения к файлу hosts. + +```bsl +.ЯвляетсяПетлевым() +``` + +Метод возвращает `Истина` для: + +- IPv4-адресов из `127.0.0.0/8` по + [RFC 1122 §3.2.1.3](https://www.rfc-editor.org/rfc/rfc1122.html#section-3.2.1.3); +- IPv6-адреса `::1` по + [RFC 4291 §2.5.3](https://www.rfc-editor.org/rfc/rfc4291.html#section-2.5.3); +- `localhost`, `localhost.` и имён внутри `.localhost` без учёта регистра по + [RFC 6761 §6.3](https://www.rfc-editor.org/rfc/rfc6761.html#section-6.3). + +При распознавании дерева `localhost` процентное кодирование незарезервированных символов не меняет +результат. Например, `local%68ost` распознаётся как `localhost` по правилам +[RFC 3986 §6.2.2.2](https://www.rfc-editor.org/rfc/rfc3986.html#section-6.2.2.2). + +IPv6-адрес с отображённым IPv4 по +[RFC 4291 §2.5.5.2](https://www.rfc-editor.org/rfc/rfc4291.html#section-2.5.5.2) +классифицируется по вложенному IPv4-адресу. Например, `::ffff:127.0.0.1` считается петлевым, +а `::ffff:192.0.2.1` нет. + +#### Возвращаемое значение + +`Булево`: `Истина`, если хост является петлевым. + +#### Ошибки + +| Код | Условие | +| --- | --- | +| `encoding.invalid` | Процентные байты имени не образуют корректную последовательность UTF-8 | +| `idna.data.invalid` | Встроенные данные IDNA повреждены или несовместимы | + +#### Ограничения + +Метод классифицирует только текст URL и не является защитой от SSRF. Другое доменное имя может +разрешиться в петлевой или иной специальный IP-адрес, а результат DNS может измениться между проверкой и +подключением. + ### КодированныйХост Возвращает кодированный хост без преобразования в Punycode. Квадратные скобки IP-литерала сохраняются. diff --git "a/src/\320\232\320\273\320\260\321\201\321\201\321\213/URL.os" "b/src/\320\232\320\273\320\260\321\201\321\201\321\213/URL.os" index 18059e4..aa273bf 100644 --- "a/src/\320\232\320\273\320\260\321\201\321\201\321\213/URL.os" +++ "b/src/\320\232\320\273\320\260\321\201\321\201\321\213/URL.os" @@ -1,4 +1,5 @@ #Использовать errors +#Использовать ip #Использовать "../internal" Перем _Схема; // Строка, Неопределено @@ -268,6 +269,51 @@ КонецФункции +// Проверяет, указывает ли хост URL на сетевой интерфейс обратной петли. +// Распознаёт IPv4 из 127.0.0.0/8, IPv6 ::1 и дерево имён localhost без сетевых запросов. +// +// Ссылки: +// RFC 1122 §3.2.1.3 - петлевые IPv4-адреса: +// https://www.rfc-editor.org/rfc/rfc1122.html#section-3.2.1.3 +// RFC 4291 §2.5.3 - петлевой IPv6-адрес: +// https://www.rfc-editor.org/rfc/rfc4291.html#section-2.5.3 +// RFC 4291 §2.5.5.2 - IPv6-адреса с отображённым IPv4: +// https://www.rfc-editor.org/rfc/rfc4291.html#section-2.5.5.2 +// RFC 6761 §6.3 - специальные имена localhost: +// https://www.rfc-editor.org/rfc/rfc6761.html#section-6.3 +// +// Возвращаемое значение: +// Булево - Истина, если явно заданный хост является петлевым. +// +// Исключения: +// encoding.invalid - Процентные байты имени не образуют корректный UTF-8. +// idna.data.invalid - Встроенные данные IDNA повреждены или несовместимы. +Функция ЯвляетсяПетлевым() Экспорт + + Если _ВидХоста = ВидыХостаURL.IPv4() Тогда + Возврат Новый IPАдрес(_КодированныйХост).ЭтоПетлевой(); + КонецЕсли; + + Если _ВидХоста = ВидыХостаURL.IPv6() Тогда + ТекстАдреса = Сред(_КодированныйХост, 2, СтрДлина(_КодированныйХост) - 2); + Адрес = Новый IPАдрес(ТекстАдреса); + Возврат Адрес.ЭтоПетлевой(); + КонецЕсли; + + Если _ВидХоста <> ВидыХостаURL.ЗарегистрированноеИмя() Тогда + Возврат Ложь; + КонецЕсли; + + ИмяХоста = НРег(Хост()); + + Если Прав(ИмяХоста, 1) = "." Тогда + ИмяХоста = Лев(ИмяХоста, СтрДлина(ИмяХоста) - 1); + КонецЕсли; + + Возврат ИмяХоста = "localhost" Или Прав(ИмяХоста, 10) = ".localhost"; + +КонецФункции + // Возвращает кодированный хост по RFC 3986 §3.2.2. // // Возвращаемое значение: diff --git "a/tests/URL/\320\237\320\265\321\202\320\273\320\265\320\262\320\276\320\271URL.os" "b/tests/URL/\320\237\320\265\321\202\320\273\320\265\320\262\320\276\320\271URL.os" new file mode 100644 index 0000000..1be2380 --- /dev/null +++ "b/tests/URL/\320\237\320\265\321\202\320\273\320\265\320\262\320\276\320\271URL.os" @@ -0,0 +1,130 @@ +#Использовать asserts +#Использовать "../../src" +#Использовать "../fixtures" + +Перем _АдресСНедопустимымUTF8; // URL + +#Область Тесты + +&ПараметризованныйТест +&ИсточникЗначение("http://127.0.0.0/", Истина) +&ИсточникЗначение("http://127.0.0.1/", Истина) +&ИсточникЗначение("http://127.255.255.255/", Истина) +&ИсточникЗначение("http://126.255.255.255/", Ложь) +&ИсточникЗначение("http://128.0.0.0/", Ложь) +Процедура ТестДолжен_ОпределитьПетлевойIPv4(СтрокаАдреса, ОжидаетсяПетлевой) Экспорт + + // Подготовка + Адрес = Новый URL(СтрокаАдреса); + + // Действие + Результат = Адрес.ЯвляетсяПетлевым(); + + // Проверка + Ожидаем.Что(Результат).Равно(ОжидаетсяПетлевой); + +КонецПроцедуры + +&ПараметризованныйТест +&ИсточникЗначение("http://[::1]/", Истина) +&ИсточникЗначение("http://[0:0:0:0:0:0:0:1]/", Истина) +&ИсточникЗначение("http://[::2]/", Ложь) +&ИсточникЗначение("http://[::ffff:127.0.0.1]/", Истина) +&ИсточникЗначение("http://[::ffff:127.255.255.255]/", Истина) +&ИсточникЗначение("http://[::ffff:192.0.2.1]/", Ложь) +Процедура ТестДолжен_ОпределитьПетлевойIPv6(СтрокаАдреса, ОжидаетсяПетлевой) Экспорт + + // Подготовка + Адрес = Новый URL(СтрокаАдреса); + + // Действие + Результат = Адрес.ЯвляетсяПетлевым(); + + // Проверка + Ожидаем.Что(Результат).Равно(ОжидаетсяПетлевой); + +КонецПроцедуры + +&ПараметризованныйТест +&ИсточникЗначение("http://localhost/", Истина) +&ИсточникЗначение("http://LOCALHOST/", Истина) +&ИсточникЗначение("http://localhost./", Истина) +&ИсточникЗначение("http://api.localhost/", Истина) +&ИсточникЗначение("http://api.localhost./", Истина) +&ИсточникЗначение("http://local%68ost/", Истина) +&ИсточникЗначение("http://api%2Elocalhost/", Истина) +&ИсточникЗначение("http://example.com/", Ложь) +&ИсточникЗначение("http://notlocalhost/", Ложь) +&ИсточникЗначение("http://localhost.example/", Ложь) +Процедура ТестДолжен_ОпределитьПетлевоеИмя(СтрокаАдреса, ОжидаетсяПетлевой) Экспорт + + // Подготовка + Адрес = Новый URL(СтрокаАдреса); + + // Действие + Результат = Адрес.ЯвляетсяПетлевым(); + + // Проверка + Ожидаем.Что(Результат).Равно(ОжидаетсяПетлевой); + +КонецПроцедуры + +&ПараметризованныйТест +&ИсточникЗначение("path", Ложь) +&ИсточникЗначение("//", Ложь) +&ИсточникЗначение("http://192.0.2.1/", Ложь) +&ИсточникЗначение("http://[2001:db8::1]/", Ложь) +&ИсточникЗначение("x://[v1.example]/", Ложь) +Процедура ТестДолжен_ОтклонитьНепетлевойХост(СтрокаАдреса, ОжидаетсяПетлевой) Экспорт + + // Подготовка + Адрес = Новый URL(СтрокаАдреса); + + // Действие + Результат = Адрес.ЯвляетсяПетлевым(); + + // Проверка + Ожидаем.Что(Результат).Равно(ОжидаетсяПетлевой); + +КонецПроцедуры + +&Тест +Процедура ТестДолжен_СохранитьСостояниеURLПриПроверке() Экспорт + + // Подготовка + Адрес = Новый URL("http://user:pass@localhost:8080/path?query#fragment"); + ИсходноеПредставление = Адрес.ВСтроку(); + + // Действие + Адрес.ЯвляетсяПетлевым(); + + // Проверка + Ожидаем.Что(Адрес.ВСтроку()).Равно(ИсходноеПредставление); + +КонецПроцедуры + +&Тест +Процедура ТестДолжен_СохранитьОшибкуДекодированияХоста() Экспорт + + // Подготовка + _АдресСНедопустимымUTF8 = Новый URL("http://%FF/"); + + // Действие и Проверка + ПомощникТестированияURL.ПроверитьКодОшибки( + Новый Действие(ЭтотОбъект, "ПроверитьАдресСНедопустимымUTF8"), + "encoding.invalid" + ); + +КонецПроцедуры + +#КонецОбласти + +#Область СлужебныеПроцедурыИФункции + +Процедура ПроверитьАдресСНедопустимымUTF8() Экспорт + + _АдресСНедопустимымUTF8.ЯвляетсяПетлевым(); + +КонецПроцедуры + +#КонецОбласти