From 4c898987b2ce425e01b6ea9e8310f9a5690379cf Mon Sep 17 00:00:00 2001 From: Dmitry Ivanov Date: Wed, 8 Oct 2025 02:32:31 +0300 Subject: [PATCH] =?UTF-8?q?feat!:=20=D0=A0=D0=B5=D1=84=D0=B0=D0=BA=D1=82?= =?UTF-8?q?=D0=BE=D1=80=D0=B8=D0=BD=D0=B3=20api?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packagedef | 2 + ...20\275\321\202\321\200\320\276\320\273.os" | 138 ++++----- ...21\201\321\202\320\270\321\202\321\214.os" | 6 +- ...21\206\320\265\321\201\321\201\320\260.os" | 18 +- .../HttpBin.os" | 270 +++++++++++------- ...20\262\320\265\321\202\320\276\320\262.os" | 267 +++++++++++------ tests/HttpBin_API_test.os | 4 +- tests/HttpBin_test.os | 21 +- 8 files changed, 446 insertions(+), 280 deletions(-) diff --git a/packagedef b/packagedef index d64c06a..fdce502 100644 --- a/packagedef +++ b/packagedef @@ -2,6 +2,7 @@ .Версия("1.3.0") .Автор("Dmitry Ivanov") .АдресАвтора("https://github.com/Stivo182") + .АдресРепозитория("https://github.com/Stivo182/oscript-httpbin") .Описание("Сервис тестирования HTTP клиента") .ВерсияСреды("1.9.2") .ВключитьФайл("src") @@ -18,6 +19,7 @@ .ЗависитОт("1connector", "2.3.3") .ЗависитОт("packageinfo", "1.1.1") .ЗависитОт("fs", "1.2.0") + .ЗависитОт("logos", "1.7.1") .РазработкаЗависитОт("1testrunner") .РазработкаЗависитОт("asserts") .РазработкаЗависитОт("coverage") diff --git "a/src/app/\320\236\321\201\320\275\320\276\320\262\320\275\320\276\320\271\320\232\320\276\320\275\321\202\321\200\320\276\320\273.os" "b/src/app/\320\236\321\201\320\275\320\276\320\262\320\275\320\276\320\271\320\232\320\276\320\275\321\202\321\200\320\276\320\273.os" index 0fb2440..ef2538b 100644 --- "a/src/app/\320\236\321\201\320\275\320\276\320\262\320\275\320\276\320\271\320\232\320\276\320\275\321\202\321\200\320\276\320\273.os" +++ "b/src/app/\320\236\321\201\320\275\320\276\320\262\320\275\320\276\320\271\320\232\320\276\320\275\321\202\321\200\320\276\320\273.os" @@ -1,8 +1,8 @@ #Использовать "../internal" #Использовать 1connector -Перем Помощник; // ПомощникПодготовкиОтветов -Перем КаталогШаблонов; // Строка +Перем _Помощник; // ПомощникПодготовкиОтветов +Перем _КаталогШаблонов; // Строка #Область ТочкиМаршрута @@ -24,7 +24,7 @@ &GET Процедура ТочкаRobots(Ответ) Экспорт - Ответ.ТелоТекст = Помощник.ТекстRobots; + Ответ.ТелоТекст = _Помощник.ТекстRobots; КонецПроцедуры @@ -32,7 +32,7 @@ &GET Процедура ТочкаDeny(Ответ) Экспорт - Ответ.ТелоТекст = Помощник.ASCII_Deny; + Ответ.ТелоТекст = _Помощник.ASCII_Deny; Ответ.Заголовки.Удалить("Content-Type"); КонецПроцедуры @@ -41,8 +41,8 @@ &GET Процедура ТочкаIP(Запрос, Ответ) Экспорт - Данные = Помощник.ПолучитьДанныеЗапроса("origin", Запрос); - Помощник.ЗаполнитьОтветJson(Ответ, Данные); + Данные = _Помощник.ПолучитьДанныеЗапроса("origin", Запрос); + _Помощник.ЗаполнитьОтветJson(Ответ, Данные); КонецПроцедуры @@ -52,7 +52,7 @@ Данные = Новый Структура("uuid", Строка(Новый УникальныйИдентификатор())); - Помощник.ЗаполнитьОтветJson(Ответ, Данные); + _Помощник.ЗаполнитьОтветJson(Ответ, Данные); КонецПроцедуры @@ -60,7 +60,7 @@ &GET Процедура ТочкаUuidN(Ответ, Знач Количество) Экспорт - Количество = Помощник.ВЧисло(Количество); + Количество = _Помощник.ВЧисло(Количество); Массив = Новый Массив(); Пока Количество > 0 Цикл @@ -70,7 +70,7 @@ Данные = Новый Структура("uuid", Массив); - Помощник.ЗаполнитьОтветJson(Ответ, Данные); + _Помощник.ЗаполнитьОтветJson(Ответ, Данные); КонецПроцедуры @@ -78,8 +78,8 @@ &GET Процедура ТочкаHeaders(Запрос, Ответ) Экспорт - Данные = Помощник.ПолучитьДанныеЗапроса("headers", Запрос); - Помощник.ЗаполнитьОтветJson(Ответ, Данные); + Данные = _Помощник.ПолучитьДанныеЗапроса("headers", Запрос); + _Помощник.ЗаполнитьОтветJson(Ответ, Данные); КонецПроцедуры @@ -87,12 +87,12 @@ &GET Процедура ТочкаUserAgent(Запрос, Ответ) Экспорт - UserAgent = Помощник.ЗначениеЗаголовка(Запрос.Заголовки, "User-Agent"); + UserAgent = _Помощник.ЗначениеЗаголовка(Запрос.Заголовки, "User-Agent"); Результат = Новый Соответствие(); Результат.Вставить("user-agent", UserAgent); - Помощник.ЗаполнитьОтветJson(Ответ, Результат); + _Помощник.ЗаполнитьОтветJson(Ответ, Результат); КонецПроцедуры @@ -105,11 +105,11 @@ &ТочкаМаршрута("anything") Процедура ТочкаAnything(Запрос, ДанныеФормы, Ответ) Экспорт - Данные = Помощник.ПолучитьДанныеЗапроса("args, data, files, form, headers, json, method, origin, url", + Данные = _Помощник.ПолучитьДанныеЗапроса("args, data, files, form, headers, json, method, origin, url", Запрос, ДанныеФормы); - Помощник.ЗаполнитьОтветJson(Ответ, Данные); + _Помощник.ЗаполнитьОтветJson(Ответ, Данные); КонецПроцедуры @@ -150,14 +150,14 @@ &ТочкаМаршрута("redirect-to") Процедура ТочкаRedirectTo(Запрос, ДанныеФормы, Ответ) Экспорт - Данные = Помощник.ПолучитьДанныеЗапроса("args, form", Запрос, ДанныеФормы); + Данные = _Помощник.ПолучитьДанныеЗапроса("args, form", Запрос, ДанныеФормы); Если Запрос.Метод = "GET" Тогда URL = Данные["args"].Получить("url"); - КодСостояния = Помощник.ВЧисло(Данные["args"].Получить("status_code")); + КодСостояния = _Помощник.ВЧисло(Данные["args"].Получить("status_code")); Иначе URL = Данные["form"].Получить("url"); - КодСостояния = Помощник.ВЧисло(Данные["form"].Получить("status_code")); + КодСостояния = _Помощник.ВЧисло(Данные["form"].Получить("status_code")); КонецЕсли; Если КодСостояния >= КодыСостоянияHTTP.МножествоВыборов_300 @@ -192,8 +192,8 @@ Если Не СтрНайти(КодыСостояний, ",") Тогда - РазделеннаяСтрока = Помощник.РазделитьСтроку(КодыСостояний, ":"); - КодСостояния = Помощник.ВЧисло(РазделеннаяСтрока.Лево); + РазделеннаяСтрока = _Помощник.РазделитьСтроку(КодыСостояний, ":"); + КодСостояния = _Помощник.ВЧисло(РазделеннаяСтрока.Лево); Если КодСостояния = 0 Тогда Ответ.УстановитьСостояние(КодыСостоянияHTTP.НеверныйЗапрос_400); @@ -206,9 +206,9 @@ ВзвешенныйСписок = Новый Массив(); Для каждого Строка Из СтрРазделить(КодыСостояний, ",") Цикл - РазделеннаяСтрока = Помощник.РазделитьСтроку(Строка, ":"); + РазделеннаяСтрока = _Помощник.РазделитьСтроку(Строка, ":"); - КодСостояния = Помощник.ВЧисло(РазделеннаяСтрока.Лево); + КодСостояния = _Помощник.ВЧисло(РазделеннаяСтрока.Лево); Если КодСостояния = 0 Тогда Ответ.УстановитьСостояние(КодыСостоянияHTTP.НеверныйЗапрос_400); Ответ.ТелоТекст = "Invalid status code"; @@ -218,17 +218,17 @@ Если ПустаяСтрока(РазделеннаяСтрока.Право) Тогда Вес = 1; Иначе - Вес = Помощник.ВЧисло(РазделеннаяСтрока.Право); + Вес = _Помощник.ВЧисло(РазделеннаяСтрока.Право); КонецЕсли; ВзвешенныйСписок.Добавить(Новый Структура("Значение, Вес", КодСостояния, Вес)); КонецЦикла; - КодСостояния = Помощник.ВыбратьСлучайныйЭлементСУчетомВеса(ВзвешенныйСписок); + КодСостояния = _Помощник.ВыбратьСлучайныйЭлементСУчетомВеса(ВзвешенныйСписок); КонецЕсли; - Помощник.ЗаполнитьОтветПоСостоянию(Ответ, КодСостояния); + _Помощник.ЗаполнитьОтветПоСостоянию(Ответ, КодСостояния); КонецПроцедуры @@ -240,10 +240,10 @@ Ответ.УстановитьТипКонтента("json"); Данные = Новый Соответствие(); - Помощник.ДополнитьСоответствие(Данные, Ответ.Заголовки); - Помощник.ДополнитьСоответствие(Данные, Запрос.ПараметрыИменные); + _Помощник.ДополнитьСоответствие(Данные, Ответ.Заголовки); + _Помощник.ДополнитьСоответствие(Данные, Запрос.ПараметрыИменные); - Помощник.ЗаполнитьОтветJson(Ответ, Данные); + _Помощник.ЗаполнитьОтветJson(Ответ, Данные); КонецПроцедуры @@ -257,7 +257,7 @@ КонецЦикла; Куки.Удалить("SessionID"); - Помощник.ЗаполнитьОтветJson(Ответ, Новый Структура("cookies", Куки)); + _Помощник.ЗаполнитьОтветJson(Ответ, Новый Структура("cookies", Куки)); КонецПроцедуры @@ -309,14 +309,14 @@ &GET Процедура ТочкаBasicAuth(Запрос, Ответ, ИмяПользователя, Пароль) Экспорт - ДанныеАутентификации = Помощник.ДанныеАутентификации(Запрос); + ДанныеАутентификации = _Помощник.ДанныеАутентификации(Запрос); Если ДанныеАутентификации.Тип = "basic" И ДанныеАутентификации.ИмяПользователя = ИмяПользователя И ДанныеАутентификации.Пароль = Пароль Тогда - Помощник.ЗаполнитьОтветJson(Ответ, Новый Структура("authenticated, user", Истина, ИмяПользователя)); + _Помощник.ЗаполнитьОтветJson(Ответ, Новый Структура("authenticated, user", Истина, ИмяПользователя)); Иначе - Помощник.ЗаполнитьОтветПоСостоянию(Ответ, КодыСостоянияHTTP.НеАвторизован_401); + _Помощник.ЗаполнитьОтветПоСостоянию(Ответ, КодыСостоянияHTTP.НеАвторизован_401); КонецЕсли; КонецПроцедуры @@ -325,12 +325,12 @@ &GET Процедура ТочкаHiddenBasicAuth(Запрос, Ответ, ИмяПользователя, Пароль) Экспорт - ДанныеАутентификации = Помощник.ДанныеАутентификации(Запрос); + ДанныеАутентификации = _Помощник.ДанныеАутентификации(Запрос); Если ДанныеАутентификации.Тип = "basic" И ДанныеАутентификации.ИмяПользователя = ИмяПользователя И ДанныеАутентификации.Пароль = Пароль Тогда - Помощник.ЗаполнитьОтветJson(Ответ, Новый Структура("authenticated, user", Истина, ИмяПользователя)); + _Помощник.ЗаполнитьОтветJson(Ответ, Новый Структура("authenticated, user", Истина, ИмяПользователя)); Иначе Ответ.УстановитьСостояние(КодыСостоянияHTTP.НеНайдено_404); КонецЕсли; @@ -341,10 +341,10 @@ &GET Процедура ТочкаBearer(Запрос, Ответ) Экспорт - ДанныеАутентификации = Помощник.ДанныеАутентификации(Запрос); + ДанныеАутентификации = _Помощник.ДанныеАутентификации(Запрос); Если ДанныеАутентификации.Тип = "bearer" Тогда - Помощник.ЗаполнитьОтветJson(Ответ, Новый Структура("authenticated, token", Истина, ДанныеАутентификации.Токен)); + _Помощник.ЗаполнитьОтветJson(Ответ, Новый Структура("authenticated, token", Истина, ДанныеАутентификации.Токен)); Иначе Ответ.Заголовки["WWW-Authenticate"] = "Bearer"; Ответ.УстановитьСостояние(КодыСостоянияHTTP.НеАвторизован_401); @@ -355,7 +355,7 @@ &ТочкаМаршрута("delay/{Секунд}") Процедура ТочкаDelay(Запрос, ДанныеФормы, Ответ, Секунд) Экспорт - Секунд = Помощник.ВЧисло(Секунд); + Секунд = _Помощник.ВЧисло(Секунд); Приостановить(Секунд * 1000); ПередатьПолныеДанныеВОтветJson(Запрос, ДанныеФормы, Ответ); @@ -378,11 +378,11 @@ &GET Процедура ТочкаCache(Запрос, Ответ) Экспорт - ЕстьЗаголовки = Помощник.ЗначениеЗаголовка(Запрос.Заголовки, "If-Modified-Since") <> Неопределено - Или Помощник.ЗначениеЗаголовка(Запрос.Заголовки, "If-None-Match") <> Неопределено; + ЕстьЗаголовки = _Помощник.ЗначениеЗаголовка(Запрос.Заголовки, "If-Modified-Since") <> Неопределено + Или _Помощник.ЗначениеЗаголовка(Запрос.Заголовки, "If-None-Match") <> Неопределено; Если Не ЕстьЗаголовки Тогда - Ответ.Заголовки["Last-Modified"] = Помощник.ДатаHTTP(); + Ответ.Заголовки["Last-Modified"] = _Помощник.ДатаВФорматеHTTP(ТекущаяУниверсальнаяДата()); Ответ.Заголовки["ETag"] = СтрЗаменить(Новый УникальныйИдентификатор(), "-", ""); ПередатьДанныеВОтветJsonGet(Запрос, Ответ); @@ -396,7 +396,7 @@ &GET Процедура ТочкаУстановитьCacheControl(Запрос, Ответ, Секунд) Экспорт - Секунд = Помощник.ВЧисло(Секунд); + Секунд = _Помощник.ВЧисло(Секунд); Ответ.Заголовки["Cache-Control"] = СтрШаблон("public, max-age=%1", Формат(Секунд, "ЧГ=")); @@ -408,11 +408,11 @@ &GET Процедура ТочкаETag(Запрос, Ответ, ETag) Экспорт - ЗаголовокIfNoneMatch = Помощник.ЗначениеЗаголовка(Запрос.Заголовки, "If-None-Match"); - ЗаголовокIfMatch = Помощник.ЗначениеЗаголовка(Запрос.Заголовки, "If-Match"); + ЗаголовокIfNoneMatch = _Помощник.ЗначениеЗаголовка(Запрос.Заголовки, "If-None-Match"); + ЗаголовокIfMatch = _Помощник.ЗначениеЗаголовка(Запрос.Заголовки, "If-Match"); - ЗначенияIfNoneMatch = Помощник.РаспаристьМногозначныйЗаголовок(ЗаголовокIfNoneMatch); - ЗначенияIfMatch = Помощник.РаспаристьМногозначныйЗаголовок(ЗаголовокIfMatch); + ЗначенияIfNoneMatch = _Помощник.РаспаристьМногозначныйЗаголовок(ЗаголовокIfNoneMatch); + ЗначенияIfMatch = _Помощник.РаспаристьМногозначныйЗаголовок(ЗаголовокIfMatch); Если ЗначениеЗаполнено(ЗначенияIfNoneMatch) Тогда Если ЗначенияIfNoneMatch.Найти(ETag) <> Неопределено Или ЗначенияIfNoneMatch.Найти("*") <> Неопределено Тогда @@ -444,11 +444,11 @@ &GET Процедура ТочкаBytes(ПараметрыЗапросаИменные, Ответ, КоличествоБайт) Экспорт - КоличествоБайт = Помощник.ВЧисло(КоличествоБайт); + КоличествоБайт = _Помощник.ВЧисло(КоличествоБайт); НачальноеЧисло = ПараметрыЗапросаИменные.Получить("seed"); Если Не НачальноеЧисло = Неопределено Тогда - НачальноеЧисло = Помощник.ВЧисло(НачальноеЧисло); + НачальноеЧисло = _Помощник.ВЧисло(НачальноеЧисло); ГенераторСлучайныхЧисел = Новый ГенераторСлучайныхЧисел(НачальноеЧисло); Иначе ГенераторСлучайныхЧисел = Новый ГенераторСлучайныхЧисел(); @@ -473,8 +473,8 @@ &GET Процедура ТочкаLinks(Ответ, Количество, Смещение) Экспорт - Количество = Помощник.ВЧисло(Количество); - Смещение = Помощник.ВЧисло(Смещение); + Количество = _Помощник.ВЧисло(Количество); + Смещение = _Помощник.ВЧисло(Смещение); Html = Новый Массив(); Html.Добавить("Links"); @@ -504,7 +504,7 @@ &GET Процедура ТочкаLinksRedirect(Ответ, Количество) Экспорт - Количество = Помощник.ВЧисло(Количество); + Количество = _Помощник.ВЧисло(Количество); URL = СтрШаблон("/links/%1/0", Формат(Количество, "ЧГ=")); Ответ.Перенаправить(URL); @@ -515,7 +515,7 @@ &GET Процедура ТочкаImage(Запрос, Ответ) Экспорт - Accept = Помощник.ЗначениеЗаголовка(Запрос.Заголовки, "Accept"); + Accept = _Помощник.ЗначениеЗаголовка(Запрос.Заголовки, "Accept"); Если Accept = "image/webp" Тогда ТочкаImageWebp(Ответ); @@ -528,7 +528,7 @@ Или Accept = "*/*" Тогда ТочкаImagePng(Ответ); Иначе - Помощник.ЗаполнитьОтветПоСостоянию(Ответ, КодыСостоянияHTTP.Неприемлемо_406); + _Помощник.ЗаполнитьОтветПоСостоянию(Ответ, КодыСостоянияHTTP.Неприемлемо_406); КонецЕсли; КонецПроцедуры @@ -593,29 +593,29 @@ &ТочкаМаршрута("gzip") &GET Процедура ТочкаGZip(Запрос, Ответ) Экспорт - Данные = Помощник.ПолучитьДанныеЗапроса("gzipped, headers, method, origin", Запрос); - Помощник.ЗаполнитьОтветJson(Ответ, Данные, "gzip"); + Данные = _Помощник.ПолучитьДанныеЗапроса("gzipped, headers, method, origin", Запрос); + _Помощник.ЗаполнитьОтветJson(Ответ, Данные, "gzip"); КонецПроцедуры &ТочкаМаршрута("deflate") &GET Процедура ТочкаDeflate(Запрос, Ответ) Экспорт - Данные = Помощник.ПолучитьДанныеЗапроса("deflated, headers, method, origin", Запрос); - Помощник.ЗаполнитьОтветJson(Ответ, Данные, "deflate"); + Данные = _Помощник.ПолучитьДанныеЗапроса("deflated, headers, method, origin", Запрос); + _Помощник.ЗаполнитьОтветJson(Ответ, Данные, "deflate"); КонецПроцедуры &ТочкаМаршрута("brotli") &GET Процедура ТочкаBrotli(Запрос, Ответ) Экспорт - Данные = Помощник.ПолучитьДанныеЗапроса("brotli, headers, method, origin", Запрос); - Помощник.ЗаполнитьОтветJson(Ответ, Данные, "brotli"); + Данные = _Помощник.ПолучитьДанныеЗапроса("brotli, headers, method, origin", Запрос); + _Помощник.ЗаполнитьОтветJson(Ответ, Данные, "brotli"); КонецПроцедуры &ТочкаМаршрута("zstd") &GET Процедура ТочкаZStd(Запрос, Ответ) Экспорт - Данные = Помощник.ПолучитьДанныеЗапроса("zstd, headers, method, origin", Запрос); - Помощник.ЗаполнитьОтветJson(Ответ, Данные, "zstd"); + Данные = _Помощник.ПолучитьДанныеЗапроса("zstd, headers, method, origin", Запрос); + _Помощник.ЗаполнитьОтветJson(Ответ, Данные, "zstd"); КонецПроцедуры #КонецОбласти @@ -625,14 +625,14 @@ &Контроллер("/") Процедура ПриСозданииОбъекта(&Пластилин("ПомощникПодготовкиОтветов") ПомощникПодготовкиОтветов) - Помощник = ПомощникПодготовкиОтветов; - КаталогШаблонов = "./src/app/view"; + _Помощник = ПомощникПодготовкиОтветов; + _КаталогШаблонов = "./src/app/view"; КонецПроцедуры Процедура Redirect(Количество, ПередаватьАбсолютныйПуть, Ответ) - Количество = Помощник.ВЧисло(Количество); + Количество = _Помощник.ВЧисло(Количество); Если Количество <= 0 Тогда Ответ.УстановитьСостояние(КодыСостоянияHTTP.НеНайдено_404); @@ -648,7 +648,7 @@ КонецЕсли; Если ПередаватьАбсолютныйПуть Тогда - Адрес = Помощник.ПолучитьURL(Путь); + Адрес = _Помощник.URL(Путь); Иначе Адрес = Путь; КонецЕсли; @@ -659,25 +659,25 @@ Процедура ПередатьДанныеВОтветJsonGet(Запрос, Ответ) - Данные = Помощник.ПолучитьДанныеЗапроса("args, headers, origin, url", Запрос); - Помощник.ЗаполнитьОтветJson(Ответ, Данные); + Данные = _Помощник.ПолучитьДанныеЗапроса("args, headers, origin, url", Запрос); + _Помощник.ЗаполнитьОтветJson(Ответ, Данные); КонецПроцедуры Процедура ПередатьПолныеДанныеВОтветJson(Запрос, ДанныеФормы, Ответ) - Данные = Помощник.ПолучитьДанныеЗапроса("args, data, files, form, headers, json, origin, url", + Данные = _Помощник.ПолучитьДанныеЗапроса("args, data, files, form, headers, json, origin, url", Запрос, ДанныеФормы); - Помощник.ЗаполнитьОтветJson(Ответ, Данные); + _Помощник.ЗаполнитьОтветJson(Ответ, Данные); КонецПроцедуры Процедура ОтдатьФайл(Путь, ТипКонтента, Ответ) Разделитель = ПолучитьРазделительПути(); - ПутьКФайлу = КаталогШаблонов + Разделитель + Путь; + ПутьКФайлу = _КаталогШаблонов + Разделитель + Путь; Ответ.Заголовки["Content-Type"] = ТипКонтента; Ответ.ТелоДвоичныеДанные = Новый ДвоичныеДанные(ПутьКФайлу); diff --git "a/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260\320\227\320\260\320\277\321\203\321\201\321\202\320\270\321\202\321\214.os" "b/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260\320\227\320\260\320\277\321\203\321\201\321\202\320\270\321\202\321\214.os" index 6751fc8..dda2906 100644 --- "a/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260\320\227\320\260\320\277\321\203\321\201\321\202\320\270\321\202\321\214.os" +++ "b/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260\320\227\320\260\320\277\321\203\321\201\321\202\320\270\321\202\321\214.os" @@ -2,7 +2,7 @@ &Опция( Имя = "h host", - Описание = "Имя хоста / IP адрес сервиса" + Описание = "Имя хоста или IP-адрес сервиса" ) &ТСтрока &ПоУмолчанию("127.0.0.1") @@ -10,14 +10,14 @@ &Опция( Имя = "p port", - Описание = "Порт сервиса" + Описание = "TCP-порт сервиса" ) &ТЧисло &ПоУмолчанию(3333) Перем _Порт; // Число &Опция( - Имя = "parrent-pid", + Имя = "parent-pid", Описание = "PID родительского процесса" ) &ТЧисло diff --git "a/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\275\321\202\321\200\320\276\320\273\320\273\320\265\321\200\320\240\320\276\320\264\320\270\321\202\320\265\320\273\321\214\321\201\320\272\320\276\320\263\320\276\320\237\321\200\320\276\321\206\320\265\321\201\321\201\320\260.os" "b/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\275\321\202\321\200\320\276\320\273\320\273\320\265\321\200\320\240\320\276\320\264\320\270\321\202\320\265\320\273\321\214\321\201\320\272\320\276\320\263\320\276\320\237\321\200\320\276\321\206\320\265\321\201\321\201\320\260.os" index 62cdc82..3799356 100644 --- "a/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\275\321\202\321\200\320\276\320\273\320\273\320\265\321\200\320\240\320\276\320\264\320\270\321\202\320\265\320\273\321\214\321\201\320\272\320\276\320\263\320\276\320\237\321\200\320\276\321\206\320\265\321\201\321\201\320\260.os" +++ "b/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\275\321\202\321\200\320\276\320\273\320\273\320\265\321\200\320\240\320\276\320\264\320\270\321\202\320\265\320\273\321\214\321\201\320\272\320\276\320\263\320\276\320\237\321\200\320\276\321\206\320\265\321\201\321\201\320\260.os" @@ -1,25 +1,32 @@ -Перем _ОжиданиеМеждуПроверками; // Число -Перем _ФоновоеЗадание; // ФоновоеЗадание, Неопределено +#Использовать logos + +Перем _ОжиданиеМеждуПроверками; // Число - Миллисекунды между проверками +Перем _ФоновоеЗадание; // ФоновоеЗадание, Неопределено +Перем _Лог; // Лог &Желудь Процедура ПриСозданииОбъекта() _ОжиданиеМеждуПроверками = 5000; + _Лог = Логирование.ПолучитьЛог("oscript.lib.httpbin"); КонецПроцедуры #Область ПрограммныйИнтерфейс // Запускает фоновое наблюдение за процессом с указанным идентификатором. -// Если процесс завершается, текущий процесс также будет автоматически завершен. +// Если наблюдаемый процесс завершается, текущий процесс автоматически завершится. // // Параметры: -// PID - Число - Идентификатор процесса, за которым необходимо наблюдать +// PID - Число - Идентификатор родительского процесса для наблюдения +// Процедура НачатьНаблюдение(PID) Экспорт Если Не _ФоновоеЗадание = Неопределено И _ФоновоеЗадание.Состояние = СостояниеФоновогоЗадания.Активно Тогда - ВызватьИсключение "Наблюдение за родительским процессом уже запущено"; + ВызватьИсключение "Невозможно запустить наблюдение: уже активно наблюдение за процессом."; КонецЕсли; + _Лог.Отладка("Запуск наблюдения за родительским процессом PID=%1", PID); + ПараметрыМетода = Новый Массив(); ПараметрыМетода.Добавить(PID); @@ -44,6 +51,7 @@ Процесс = НайтиПроцессПоИдентификатору(PID); Если Процесс = Неопределено Тогда + _Лог.Отладка("Родительский процесс PID=%1 завершён. Завершение текущего процесса", PID); ТекущийПроцесс().Завершить(); КонецЕсли; diff --git "a/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/HttpBin.os" "b/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/HttpBin.os" index 1bcdabd..f9eb541 100644 --- "a/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/HttpBin.os" +++ "b/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/HttpBin.os" @@ -3,79 +3,79 @@ #Использовать 1connector #Использовать fs +#Использовать logos -Перем _ЗапускатьВФоне; // Булево -Перем _ОжидатьЗапуск; // Булево -Перем _ТаймаутЗапуска; // Число -Перем _ТаймаутСоединенияHttp; // Число -Перем _ИмяХоста; // Строка -Перем _Порт; // Число -Перем _Процесс; // Процесс, Неопределено +Перем _ТаймаутЗапуска; // Число - Таймаут запуска сервиса в секундах +Перем _ТаймаутПроверки; // Число - Таймаут HTTP-проверки доступности в секундах +Перем _ИмяХоста; // Строка - IP-адрес или доменное имя хоста +Перем _Порт; // Число - Номер TCP-порта сервиса +Перем _Процесс; // Процесс, Неопределено - Объект запущенного процесса +Перем _Лог; // Лог - Логгер для отладочной информации #Область ПрограммныйИнтерфейс -// Запускает сервис +// Запускает HTTP-сервис в синхронном режиме с ожиданием полной готовности. +// Блокирует выполнение до тех пор, пока сервис не начнет отвечать на запросы +// или не истечет таймаут запуска. // // Возвращаемое значение: -// ЭтотОбъект +// ЭтотОбъект - Для возможности цепочки вызовов +// Функция Запустить() Экспорт + Возврат ЗапуститьПроцесс(Истина); +КонецФункции - Если Активен() Тогда - ВызватьИсключение "Процесс уже запущен"; - КонецЕсли; - - РабочийКаталог = ФС.НормализоватьПуть(ОбъединитьПути(ТекущийСценарий().Каталог, "../../..")); - ИсполняемыйФайл = НайтиИсполняемыйФайл(); - - СтрокаКоманды = Новый Массив(); - СтрокаКоманды.Добавить(ОбернутьВДвойныеКавычки(ИсполняемыйФайл)); - СтрокаКоманды.Добавить(ОбернутьВКавычки(ТочкаВходаКонсольногоПриложения())); - СтрокаКоманды.Добавить("run"); - СтрокаКоманды.Добавить("--host"); - СтрокаКоманды.Добавить(_ИмяХоста); - СтрокаКоманды.Добавить("--port"); - СтрокаКоманды.Добавить(Формат(_Порт, "ЧГ=")); - СтрокаКоманды.Добавить("--parrent-pid"); - СтрокаКоманды.Добавить(Формат(ТекущийПроцесс().Идентификатор, "ЧГ=")); - СтрокаКоманды = СтрСоединить(СтрокаКоманды, " "); - - _Процесс = СоздатьПроцесс(СтрокаКоманды, РабочийКаталог, , , , ПеременныеСреды()); - _Процесс.Запустить(); +// Запускает HTTP-сервис в асинхронном режиме без ожидания готовности. +// Возвращает управление немедленно после старта процесса. +// Используйте метод Отвечает() для проверки готовности сервиса. +// +// Возвращаемое значение: +// ЭтотОбъект - Для возможности цепочки вызовов +// +Функция ЗапуститьАсинх() Экспорт + Возврат ЗапуститьПроцесс(Ложь); +КонецФункции - Если _ОжидатьЗапуск Тогда - НачатьОжиданиеЗапуска(); +// Останавливает работающий HTTP-сервис. +// Если сервис не запущен, метод ничего не делает. +// +// Возвращаемое значение: +// ЭтотОбъект - Для возможности цепочки вызовов +// +Функция Остановить() Экспорт - Если Не Активен() Тогда - ВызватьИсключение СтрШаблон("Не удалось запустить сервис по адресу %1:%2", Хост(), Порт()); - КонецЕсли; - КонецЕсли; - - Если Не _ЗапускатьВФоне Тогда - _Процесс.ОжидатьЗавершения(); + Если Активен() Тогда + _Процесс.Завершить(); КонецЕсли; Возврат ЭтотОбъект; - + КонецФункции -// Останавливает сервис +// Ожидает завершения работы сервиса. +// Блокирует выполнение до тех пор, пока процесс сервиса не завершится. // // Возвращаемое значение: -// ЭтотОбъект -Функция Остановить() Экспорт +// ЭтотОбъект - Для возможности цепочки вызовов +// +Функция ОжидатьЗавершения() Экспорт - Если Активен() Тогда - _Процесс.Завершить(); + Если Не Активен() Тогда + ВызватьИсключение "Невозможно ожидать завершения: процесс сервиса не запущен"; КонецЕсли; + _Процесс.ОжидатьЗавершения(); + Возврат ЭтотОбъект; КонецФункции -// Проверяет, что сервис отвечает на HTTP запросы без ошибок +// Проверяет доступность сервиса через HTTP-запрос. +// Выполняет HEAD-запрос к корневому URL сервиса и проверяет код ответа. // // Возвращаемое значение: -// Булево +// Булево - Истина, если сервис доступен и возвращает HTTP 200 OK +// Функция Отвечает() Экспорт Если Не Активен() Тогда @@ -85,9 +85,14 @@ КодСостояния = ""; Попытка - Ответ = КоннекторHTTP.Head(URL(), Новый Структура("Таймаут", _ТаймаутСоединенияHttp)); + Ответ = КоннекторHTTP.Head(URL(), Новый Структура("Таймаут", _ТаймаутПроверки)); КодСостояния = Ответ.КодСостояния; Исключение + _Лог.Отладка( + "Сервис недоступен по адресу %1. Ошибка: %2", + URL(), + ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()) + ); Возврат Ложь; КонецПопытки; @@ -95,103 +100,100 @@ КонецФункции -// Проверяет, что процесс сервиса активен +// Проверяет, что процесс сервиса запущен и не завершен. // // Возвращаемое значение: // Булево +// Функция Активен() Экспорт Возврат Не _Процесс = Неопределено И Не _Процесс.Завершен; КонецФункции -// Возвращает адрес сервиса +// Возвращает номер TCP-порта, на котором работает сервис. // // Возвращаемое значение: -// Строка -Функция URL() Экспорт - Возврат СтрШаблон("http://%1:%2", _ИмяХоста, _Порт); -КонецФункции - -// Порт сервиса +// Число - Номер порта (1-65535) // -// Возвращаемое значение: -// Строка Функция Порт() Экспорт Возврат _Порт; КонецФункции -// Устанавливает порт сервиса +// Устанавливает TCP-порт для запуска сервиса. +// Должен быть вызван до запуска сервиса. // // Параметры: -// Порт - Число - Номер порта +// Порт - Число - Номер порта (1-65535) // // Возвращаемое значение: -// ЭтотОбъект +// ЭтотОбъект - Для возможности цепочки вызовов +// Функция УстановитьПорт(Порт) Экспорт _Порт = Порт; Возврат ЭтотОбъект; КонецФункции -// Имя хоста / ip адрес сервиса +// Возвращает имя хоста или IP-адрес сервиса. // // Возвращаемое значение: -// Строка +// Строка - Имя хоста или IP-адрес (например, "127.0.0.1" или "localhost") +// Функция Хост() Экспорт Возврат _ИмяХоста; КонецФункции -// Устанавливает хост сервиса +// Устанавливает имя хоста или IP-адрес для запуска сервиса. +// Должен быть вызван до запуска сервиса. // // Параметры: -// Хост - Строка - Имя хоста / ip адрес +// Хост - Строка - Имя хоста или IP-адрес (например, "127.0.0.1", "localhost", "0.0.0.0") // // Возвращаемое значение: -// ЭтотОбъект +// ЭтотОбъект - Для возможности цепочки вызовов +// Функция УстановитьХост(Хост) Экспорт _ИмяХоста = Хост; Возврат ЭтотОбъект; КонецФункции -// Запуск сервиса будет выполнен в фоновом режиме +// Формирует полный URL-адрес сервиса с опциональным путем к ресурсу. // // Параметры: -// Флаг - Булево +// АдресРесурса - Строка - Путь к ресурсу (например, "/get"). +// Слэш в начале необязателен. // // Возвращаемое значение: -// ЭтотОбъект -Функция ЗапускатьВФоне(Флаг = Истина) Экспорт - _ЗапускатьВФоне = Флаг; - Возврат ЭтотОбъект; -КонецФункции - -// Ожидать завершение запуска сервиса +// Строка - Полный URL (например, "http://127.0.0.1:3333/get") // -// Параметры: -// Флаг - Булево -// -// Возвращаемое значение: -// ЭтотОбъект -Функция ОжидатьЗапуск(Флаг = Истина) Экспорт - _ОжидатьЗапуск = Флаг; - Возврат ЭтотОбъект; +Функция URL(Знач АдресРесурса = "") Экспорт + + Если Лев(АдресРесурса, 1) <> "/" Тогда + АдресРесурса = "/" + АдресРесурса; + КонецЕсли; + + Возврат СтрШаблон("http://%1:%2%3", _ИмяХоста, _Порт, АдресРесурса); + КонецФункции -// Устанавливает таймаут запуска сервиса, запущенного в фоновом режиме +// Устанавливает максимальное время ожидания запуска сервиса. +// Применяется при синхронном запуске через метод Запустить(). // // Параметры: -// Таймаут - Количество - Таймаут в секундах +// Таймаут - Число - Таймаут в секундах // // Возвращаемое значение: -// ЭтотОбъект -Функция УстановитьТаймаут(Таймаут) Экспорт +// ЭтотОбъект - Для возможности цепочки вызовов +// +Функция УстановитьТаймаутЗапуска(Таймаут) Экспорт _ТаймаутЗапуска = Таймаут; Возврат ЭтотОбъект; КонецФункции -// Таймаут запуска сервиса в секундах, запущенного в фоновом режиме +// Возвращает текущее значение таймаута запуска сервиса. // // Возвращаемое значение: -// Число -Функция Таймаут() Экспорт +// Число - Таймаут в секундах +// +Функция ТаймаутЗапуска() Экспорт Возврат _ТаймаутЗапуска; КонецФункции @@ -199,36 +201,82 @@ #Область СлужебныеПроцедурыИФункции -// Cервис тестирования HTTP клиента. +// Конструктор класса. +// +// По умолчанию сервис настроен на локальный адрес 127.0.0.1:3333 +// с таймаутом запуска 5 секунд. +// +// Параметры: +// Хост - Строка - IP-адрес или имя хоста (по умолчанию "127.0.0.1") +// Порт - Число - Номер TCP-порта (по умолчанию 3333) // -// Сервис по умолчанию запускается по адресу 127.0.0.1:3333 в фоновом режиме -// и с ожиданием завершения запуска сервиса. -Процедура ПриСозданииОбъекта() +Процедура ПриСозданииОбъекта(Хост = "127.0.0.1", Порт = 3333) // BSLLS:MagicNumber-off - ХостПоУмолчанию = "127.0.0.1"; - ПортПоУмолчанию = 3333; ТаймаутЗапускаПоУмолчанию = 5; - _ТаймаутСоединенияHttp = 3; + _ТаймаутПроверки = 3; - УстановитьХост(ХостПоУмолчанию); - УстановитьПорт(ПортПоУмолчанию); - ЗапускатьВФоне(); - ОжидатьЗапуск(); - УстановитьТаймаут(ТаймаутЗапускаПоУмолчанию); + УстановитьХост(Хост); + УстановитьПорт(Порт); + УстановитьТаймаутЗапуска(ТаймаутЗапускаПоУмолчанию); + + _Лог = Логирование.ПолучитьЛог("oscript.lib.httpbin"); КонецПроцедуры +Функция ЗапуститьПроцесс(Синхронно) + + Если Активен() Тогда + ВызватьИсключение "Невозможно запустить сервис: процесс уже запущен."; + КонецЕсли; + + РабочийКаталог = ФС.НормализоватьПуть(ОбъединитьПути(ТекущийСценарий().Каталог, "../../..")); + ИсполняемыйФайл = НайтиИсполняемыйФайл(); + + СтрокаКоманды = Новый Массив(); + СтрокаКоманды.Добавить(ОбернутьВДвойныеКавычки(ИсполняемыйФайл)); + СтрокаКоманды.Добавить(ОбернутьВКавычки(ТочкаВходаКонсольногоПриложения())); + СтрокаКоманды.Добавить("run"); + СтрокаКоманды.Добавить("--host"); + СтрокаКоманды.Добавить(_ИмяХоста); + СтрокаКоманды.Добавить("--port"); + СтрокаКоманды.Добавить(Формат(_Порт, "ЧГ=")); + СтрокаКоманды.Добавить("--parent-pid"); + СтрокаКоманды.Добавить(Формат(ТекущийПроцесс().Идентификатор, "ЧГ=")); + СтрокаКоманды = СтрСоединить(СтрокаКоманды, " "); + + _Лог.Отладка("Запуск процесса: %1", СтрокаКоманды); + + _Процесс = СоздатьПроцесс(СтрокаКоманды, РабочийКаталог, , , , ПеременныеСреды()); + _Процесс.Запустить(); + + Если Синхронно Тогда + НачатьОжиданиеЗапуска(); + + Если Не Активен() Тогда + ВызватьИсключение СтрШаблон("Не удалось запустить сервис по адресу %1:%2", Хост(), Порт()); + КонецЕсли; + КонецЕсли; + + Возврат ЭтотОбъект; + +КонецФункции + Процедура НачатьОжиданиеЗапуска() ЗадержкаМеждуПопытками = 100; // Миллисекунд ВремяНачала = ТекущаяУниверсальнаяДата(); + НомерПопытки = 0; Пока Истина Цикл + НомерПопытки = НомерПопытки + 1; КодСостояния = Неопределено; ТекстИсключения = ""; + + _Лог.Отладка("Проверка готовности сервиса %1. Попытка: %2", URL(), НомерПопытки); + Попытка - Ответ = КоннекторHTTP.Head(URL(), Новый Структура("Таймаут", _ТаймаутСоединенияHttp)); + Ответ = КоннекторHTTP.Head(URL(), Новый Структура("Таймаут", _ТаймаутПроверки)); КодСостояния = Ответ.КодСостояния; Исключение ТекстИсключения = СокрП(КраткоеПредставлениеОшибки(ИнформацияОбОшибке())); @@ -236,6 +284,7 @@ КонецПопытки; Если КодСостояния = КодыСостоянияHTTP.ОК_200 Тогда + _Лог.Отладка("Сервис %1 успешно запущен и готов к работе.", URL()); Прервать; КонецЕсли; @@ -243,17 +292,24 @@ Если ПрошлоСекунд > _ТаймаутЗапуска Или ЗначениеЗаполнено(КодСостояния) Тогда ТекстОшибки = СтрШаблон( - "Не удалось запустить веб-сервер по адресу %1:%2 в течение %3 сек.", + "Превышен таймаут запуска сервиса %1:%2 (%3 сек). " + + "Сервис не начал отвечать на запросы в течение отведенного времени.", _ИмяХоста, _Порт, _ТаймаутЗапуска); Если ЗначениеЗаполнено(КодСостояния) Тогда ТекстОшибки = СтрШаблон("%1: - |%2", ТекстОшибки, КодыСостоянияHTTP.Представление(КодСостояния)); + |%2", + ТекстОшибки, + КодыСостоянияHTTP.Представление(КодСостояния) + ); Иначе ТекстОшибки = СтрШаблон("%1: - |%2", ТекстОшибки, ТекстИсключения); + |%2", + ТекстОшибки, + ТекстИсключения + ); КонецЕсли; ВызватьИсключение ТекстОшибки; @@ -285,7 +341,7 @@ КонецЕсли; КонецЦикла; - ВызватьИсключение "Не удалось найти исполняемый файл oscript"; + ВызватьИсключение СтрШаблон("Не найден исполняемый файл oscript в каталоге: %1", КаталогПрограммы); КонецФункции diff --git "a/src/internal/\320\232\320\273\320\260\321\201\321\201\321\213/\320\237\320\276\320\274\320\276\321\211\320\275\320\270\320\272\320\237\320\276\320\264\320\263\320\276\321\202\320\276\320\262\320\272\320\270\320\236\321\202\320\262\320\265\321\202\320\276\320\262.os" "b/src/internal/\320\232\320\273\320\260\321\201\321\201\321\213/\320\237\320\276\320\274\320\276\321\211\320\275\320\270\320\272\320\237\320\276\320\264\320\263\320\276\321\202\320\276\320\262\320\272\320\270\320\236\321\202\320\262\320\265\321\202\320\276\320\262.os" index 187bbc7..71817a1 100644 --- "a/src/internal/\320\232\320\273\320\260\321\201\321\201\321\213/\320\237\320\276\320\274\320\276\321\211\320\275\320\270\320\272\320\237\320\276\320\264\320\263\320\276\321\202\320\276\320\262\320\272\320\270\320\236\321\202\320\262\320\265\321\202\320\276\320\262.os" +++ "b/src/internal/\320\232\320\273\320\260\321\201\321\201\321\213/\320\237\320\276\320\274\320\276\321\211\320\275\320\270\320\272\320\237\320\276\320\264\320\263\320\276\321\202\320\276\320\262\320\272\320\270\320\236\321\202\320\262\320\265\321\202\320\276\320\262.os" @@ -3,67 +3,70 @@ #Использовать 1connector #Использовать compressor -Перем ТекстRobots Экспорт; // Строка -Перем ASCII_Deny Экспорт; // Строка -Перем ASCII_Teapot; // Строка -Перем АдресРедиректа; // Строка -Перем ПоддерживаемыеМедиаТипы; // Массив из Строка +Перем ТекстRobots Экспорт; // Строка - Содержимое файла robots.txt +Перем ASCII_Deny Экспорт; // Строка - ASCII-арт для страницы отказа доступа +Перем ASCII_Teapot; // Строка - ASCII-арт чайника для HTTP 418 +Перем АдресРедиректа; // Строка - URL для редиректов (по умолчанию) +Перем ПоддерживаемыеМедиаТипы; // Массив из Строка - Список поддерживаемых MIME-типов -Перем НастройкиВебСервера; // Настройки - см. winow -Перем Парсеры; // Парсеры - см. winow -Перем ЭтоНативныйВебСервер; // Булево +Перем НастройкиВебСервера; // Настройки - Настройки веб-сервера (см. winow) +Перем Парсеры; // Парсеры - Утилиты парсинга (см. winow) +Перем ЭтоНативныйВебСервер; // Булево - Признак использования нативного веб-сервера #Область ПрограммныйИнтерфейс -// Заполняет ответ json данными +// Формирует JSON-ответ с опциональным сжатием. // // Параметры: -// Ответ - Ответ - см. winow -// Данные - Соответствие, Структура - Данные для передачи в тело ответа -// АлгоритмСжатия - Строка - Алгоритм сжатия данных (gzip, deflate, brotli, zstd) +// Ответ - Ответ - Объект ответа для заполнения (см. winow) +// Данные - Соответствие, Структура - Данные для сериализации в JSON +// АлгоритмСжатия - Строка, Неопределено - Алгоритм сжатия тела ответа (gzip, deflate, brotli, zstd) +// Процедура ЗаполнитьОтветJson(Ответ, Данные, АлгоритмСжатия = Неопределено) Экспорт Ответ.УстановитьТипКонтента("json"); ТекстJson = ВJson(Данные); - + Компрессор = Неопределено; + Если АлгоритмСжатия = "gzip" Тогда Компрессор = Новый GZipКомпрессор(); - Ответ.Заголовки["Content-Encoding"] = "gzip"; - Ответ.ТелоДвоичныеДанные = Компрессор.Упаковать(ПолучитьДвоичныеДанныеИзСтроки(ТекстJson)); + ЗаголовокСжатия = "gzip"; ИначеЕсли АлгоритмСжатия = "deflate" Тогда Компрессор = Новый DeflateКомпрессор(); - Ответ.Заголовки["Content-Encoding"] = "deflate"; - Ответ.ТелоДвоичныеДанные = Компрессор.Упаковать(ПолучитьДвоичныеДанныеИзСтроки(ТекстJson)); + ЗаголовокСжатия = "deflate"; ИначеЕсли АлгоритмСжатия = "brotli" Тогда Компрессор = Новый BrotliКомпрессор(); - Ответ.Заголовки["Content-Encoding"] = "br"; - Ответ.ТелоДвоичныеДанные = Компрессор.Упаковать(ПолучитьДвоичныеДанныеИзСтроки(ТекстJson)); + ЗаголовокСжатия = "br"; ИначеЕсли АлгоритмСжатия = "zstd" Тогда Компрессор = Новый ZStdКомпрессор(); - Ответ.Заголовки["Content-Encoding"] = "zstd"; - Ответ.ТелоДвоичныеДанные = Компрессор.Упаковать(ПолучитьДвоичныеДанныеИзСтроки(ТекстJson)); + ЗаголовокСжатия = "zstd"; - Иначе + КонецЕсли; + Если Не Компрессор = Неопределено Тогда + ДвоичныеДанные = ПолучитьДвоичныеДанныеИзСтроки(ТекстJson); + Ответ.Заголовки["Content-Encoding"] = ЗаголовокСжатия; + Ответ.ТелоДвоичныеДанные = Компрессор.Упаковать(ДвоичныеДанные); + Иначе Ответ.ТелоТекст = ТекстJson; - КонецЕсли; КонецПроцедуры -// Заполняет ответ по состоянию +// Заполняет ответ стандартным содержимым для указанного HTTP-статуса. // // Параметры: -// Ответ - Ответ - см. winow -// КодСостояния - Число - Код состояния HTTP +// Ответ - Ответ - Объект ответа для заполнения (см. winow) +// КодСостояния - Число - Код HTTP-статуса +// Процедура ЗаполнитьОтветПоСостоянию(Ответ, КодСостояния) Экспорт Ответ.УстановитьСостояние(КодСостояния); @@ -106,15 +109,32 @@ КонецПроцедуры -// Данные входящего запроса +// Извлекает и структурирует данные из входящего HTTP-запроса. +// Возвращает только указанные ключи из полного набора данных запроса. // // Параметры: -// Ключи - Строка - Ключи через запятую -// Запрос - ВходящийЗапрос - см. winow -// ДанныеФормы - ДанныеСоставнойФормы - Данные составной формы (multipart) (см. winow) +// Ключи - Строка - Имена возвращаемых полей через запятую. +// Доступные поля: url, method, args, headers, origin, json, +// data, form, files, gzipped, deflated, brotli, zstd +// Запрос - ВходящийЗапрос - Объект входящего HTTP-запроса (см. winow) +// ДанныеФормы - ДанныеСоставнойФормы, Неопределено - Данные multipart/form-data (см. winow) // // Возвращаемое значение: -// Структура +// Структура - Структура с запрошенными полями: +// * url - Строка - Полный URL запроса +// * method - Строка - HTTP-метод (GET, POST, PUT и т.д.) +// * args - Соответствие - Параметры строки запроса +// * headers - Соответствие - Заголовки запроса +// * origin - Строка - IP-адрес клиента +// * json - Соответствие, null - Распарсенное JSON-тело (если Content-Type: application/json) +// * data - Строка - Сырое тело запроса (если не form-data) +// * form - Соответствие - Данные формы (application/x-www-form-urlencoded или multipart) +// * files - Соответствие - Загруженные файлы из multipart/form-data +// * gzipped - Булево - Всегда Истина (поддержка gzip) +// * deflated - Булево - Всегда Истина (поддержка deflate) +// * brotli - Булево - Всегда Истина (поддержка brotli) +// * zstd - Булево - Всегда Истина (поддержка zstd) +// Функция ПолучитьДанныеЗапроса(Ключи, Запрос, ДанныеФормы = Неопределено) Экспорт ДанныеФормы = РазделитьДанныеФормы(ДанныеФормы); @@ -129,7 +149,7 @@ КонецЕсли; ДанныеЗапроса = Новый Структура(); - ДанныеЗапроса.Вставить("url", ПолучитьURL(Запрос.ПолныйПуть)); + ДанныеЗапроса.Вставить("url", URL(Запрос.ПолныйПуть)); ДанныеЗапроса.Вставить("method", Запрос.Метод); ДанныеЗапроса.Вставить("args", Запрос.ПараметрыИменные); ДанныеЗапроса.Вставить("headers", ЗаголовкиДляJson(Запрос.Заголовки)); @@ -149,20 +169,24 @@ КонецФункции -// Данные аутентификации из входящего запроса +// Извлекает данные аутентификации из заголовка Authorization. +// Поддерживает схемы Basic и Bearer. // // Параметры: -// Запрос - ВходящийЗапрос - см. winow +// Запрос - ВходящийЗапрос - Объект входящего HTTP-запроса (см. winow) // // Возвращаемое значение: -// Структура: -// Общее: -// * Тип - Строка - Тип аутентификации (basic, bearer) -// Basic: +// Структура - Данные аутентификации: +// Общие поля: +// * Тип - Строка - Схема аутентификации ("basic", "bearer" или пустая строка) +// +// Для Basic-аутентификации (Тип = "basic"): // * ИмяПользователя - Строка, Неопределено - Имя пользователя // * Пароль - Строка, Неопределено - Пароль -// Bearer: -// * Токен - Строка - Токен +// +// Для Bearer-токена (Тип = "bearer"): +// * Токен - Строка - Значение токена +// Функция ДанныеАутентификации(Запрос) Экспорт Данные = Новый Структура("Тип", ""); @@ -205,14 +229,15 @@ КонецФункции -// Значение заголовка по имени +// Извлекает значение HTTP-заголовка по имени (регистронезависимый поиск). // // Параметры: // Заголовки - Соответствие, СловарьЗаголовков - Коллекция заголовков -// Имя - Строка - Имя заголовка +// Имя - Строка - Имя заголовка для поиска (регистр не важен) // // Возвращаемое значение: -// Строка, Неопределено +// Строка, Соответствие, Неопределено +// Функция ЗначениеЗаголовка(Заголовки, Знач Имя) Экспорт Имя = НРег(Имя); @@ -232,13 +257,15 @@ КонецФункции -// Парсит многозначный заголовок +// Парсит многозначный HTTP-заголовок (например, If-Match, If-None-Match). +// Обрабатывает значения с кавычками и опциональным префиксом W/ (weak ETag). // // Параметры: -// Значение - Строка - Значение заголовка +// Значение - Строка - Значение заголовка с несколькими значениями через запятую // // Возвращаемое значение: -// Массив из Строка - Массив значений +// Массив из Строка - Массив извлечённых значений +// Функция РаспаристьМногозначныйЗаголовок(Значение) Экспорт Результат = Новый Массив(); @@ -261,56 +288,66 @@ КонецФункции -// Получает URL к запущенному веб-серверу с добавлением пути +// Формирует полный URL-адрес сервиса с опциональным путем к ресурсу. // // Параметры: -// ПолныйПуть - Строка - Путь после порта веб-сервера, включая "/" +// АдресРесурса - Строка - Путь к ресурсу (например, "/get"). +// Слэш в начале необязателен. // // Возвращаемое значение: -// Строка - URL -Функция ПолучитьURL(ПолныйПуть) Экспорт +// Строка - Полный URL +// +Функция URL(Знач АдресРесурса = "") Экспорт + + СтандартныйПорт = 80; + + Если Лев(АдресРесурса, 1) <> "/" Тогда + АдресРесурса = "/" + АдресРесурса; + КонецЕсли; - Возврат "http://" - + НастройкиВебСервера.ИмяХоста - + ?(НастройкиВебСервера.Порт = 80, "", ":" + Формат(НастройкиВебСервера.Порт, "ЧГ=")) - + ПолныйПуть; + Возврат СтрШаблон( + "http://%1%2%3", + НастройкиВебСервера.ИмяХоста, + ?(НастройкиВебСервера.Порт = СтандартныйПорт, "", ":" + Формат(НастройкиВебСервера.Порт, "ЧГ=")), + АдресРесурса + ); КонецФункции -// Преобразует универсальную дату в дату формата rfc1123-date. -// См. https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html, п. 3.3.1. +// Форматирует дату в формат RFC 1123 для использования в HTTP-заголовках. +// Соответствует спецификации HTTP/1.1 (RFC 2616, раздел 3.3.1). // // Параметры: -// Дата - Дата, Неопределено - Если не указано, то беретеся текущая универсальная дата +// Дата - Дата, Неопределено - Дата для форматирования // // Возвращаемое значение: -// Строка -Функция ДатаHTTP(Знач Дата = Неопределено) Экспорт - - Если Дата = Неопределено Тогда - Дата = ТекущаяУниверсальнаяДата(); - КонецЕсли; +// Строка - Дата в формате "Day, DD Mon YYYY HH:MM:SS GMT" +// +Функция ДатаВФорматеHTTP(Дата) Экспорт Возврат Формат(Дата, "Л=en; ДФ='ддд, дд МММ гггг ЧЧ:мм:сс ''GMT'''"); КонецФункции -// Получает число из строки +// Безопасно преобразует строку в число. // // Параметры: -// Значение - Строка - Число строкой +// Значение - Строка - Строковое представление числа // // Возвращаемое значение: -// Число +// Число - Преобразованное число или 0, если преобразование невозможно +// Функция ВЧисло(Значение) Экспорт Возврат Новый ОписаниеТипов("Число").ПривестиЗначение(Значение); КонецФункции -// Дополняет соответствие +// Копирует все элементы из исходного соответствия в приемник. +// Существующие ключи в приемнике будут перезаписаны значениями из источника. // // Параметры: -// Приемник - Соответствие - Приемник -// Источник - Соответствие - Источник +// Приемник - Соответствие - Соответствие, в которое копируются данные +// Источник - Соответствие - Соответствие-источник данных +// Процедура ДополнитьСоответствие(Приемник, Источник) Экспорт Для Каждого Элемент Из Источник Цикл @@ -319,15 +356,18 @@ КонецПроцедуры -// Выбирает случайный элемент с учетом веса +// Выбирает случайный элемент из взвешенного списка. +// Вероятность выбора элемента пропорциональна его весу. +// Использует бинарный поиск для эффективного выбора. // // Параметры: -// Список - Массив из Структура - Взвешенный список: -// * Значение - Произвольный - Значение -// * Вес - Число - Вес +// Список - Массив из Структура - Взвешенный список элементов: +// * Значение - Произвольный - Значение элемента для возврата +// * Вес - Число - Вес элемента (чем больше, тем выше вероятность выбора) // // Возвращаемое значение: -// Произвольный - Случайное значение из списка +// Произвольный - Случайно выбранное значение с учётом весов +// Функция ВыбратьСлучайныйЭлементСУчетомВеса(Список) Экспорт ОбщийВес = 0; @@ -355,16 +395,16 @@ КонецФункции -// Разделяет строку по разделителю +// Разделяет строку на две части по первому вхождению разделителя. // // Параметры: -// Строка - Строка -// Разделитель - Строка +// Строка - Строка - Исходная строка для разделения +// Разделитель - Строка - Строка-разделитель // // Возвращаемое значение: -// Структура: -// * Лево - Строка -// * Право - Строка +// Структура - Части строки: +// * Лево - Строка - Часть до разделителя +// * Право - Строка - Часть после разделителя Функция РазделитьСтроку(Строка, Разделитель) Экспорт Результат = Новый Структура("Лево, Право", "", ""); @@ -426,6 +466,16 @@ КонецПроцедуры +// Извлекает реальный IP-адрес клиента с учётом прокси-серверов. +// Сначала проверяет заголовок X-Forwarded-For, затем использует прямой адрес. +// Удаляет IPv6-префикс ::ffff: для IPv4-адресов. +// +// Параметры: +// Запрос - ВходящийЗапрос - Объект входящего HTTP-запроса (см. winow) +// +// Возвращаемое значение: +// Строка - IP-адрес клиента (IPv4 или IPv6) +// Функция IPАдресУдаленногоУзла(Запрос) Адрес = ЗначениеЗаголовка(Запрос.Заголовки, "X-Forwarded-For"); @@ -442,6 +492,16 @@ КонецФункции +// Парсит данные multipart/form-data и разделяет их на поля формы и файлы. +// +// Параметры: +// ДанныеФормы - ДанныеСоставнойФормы, Неопределено - Данные составной формы +// +// Возвращаемое значение: +// Структура: +// * Данные - Соответствие - Текстовые поля формы +// * Файлы - Соответствие - Загруженные файлы +// Функция РазделитьДанныеФормы(ДанныеФормы) Результат = Новый Структура(); @@ -529,6 +589,17 @@ КонецФункции +// Преобразует двоичные данные в текстовое представление. +// Для бинарных данных возвращает Data URL (base64). +// Для текстовых данных возвращает UTF-8 строку. +// +// Параметры: +// ДвоичныеДанные - ДвоичныеДанные - Данные для преобразования +// ТипКонтента - Строка - MIME-тип данных +// +// Возвращаемое значение: +// Строка +// Функция ТекстовоеПредставлениеДвоичныхДанных(ДвоичныеДанные, ТипКонтента) Если ДвоичныеДанныеСодержатУправляющиеСимволы(ДвоичныеДанные) Тогда @@ -543,6 +614,15 @@ КонецФункции +// Проверяет, содержат ли двоичные данные управляющие символы (бинарные данные). +// Анализирует первые 1024 байта для определения типа содержимого. +// +// Параметры: +// ДвоичныеДанные - ДвоичныеДанные - Данные для проверки +// +// Возвращаемое значение: +// Булево - Истина, если данные содержат управляющие символы (бинарные) +// Функция ДвоичныеДанныеСодержатУправляющиеСимволы(ДвоичныеДанные) КоличествоЧитаемыхБайтов = 1024; @@ -575,6 +655,15 @@ КонецФункции +// Нормализует заголовки для сериализации в JSON. +// Для нативного веб-сервера преобразует СтроковыеЗначения в простые строки. +// +// Параметры: +// Заголовки - Соответствие, СловарьЗаголовков - Коллекция заголовков +// +// Возвращаемое значение: +// Соответствие - Нормализованные заголовки для JSON +// Функция ЗаголовкиДляJson(Заголовки) Если ЭтоНативныйВебСервер Тогда @@ -603,6 +692,14 @@ КонецФункции +// Сериализует данные в JSON с форматированием. +// +// Параметры: +// Данные - Соответствие, Структура - Данные для сериализации +// +// Возвращаемое значение: +// Строка +// Функция ВJson(Данные) ПараметрыЗаписиJSON = Новый ПараметрыЗаписиJSON(ПереносСтрокJSON.Авто, " "); @@ -615,6 +712,16 @@ КонецФункции +// Создаёт новую структуру с указанными ключами из исходной структуры. +// Копирует только запрошенные поля. Значения null заменяются на пустые строки. +// +// Параметры: +// ИсходнаяСтруктура - Структура - Структура-источник данных +// КопируемыеКлючи - Строка - Имена копируемых полей через запятую +// +// Возвращаемое значение: +// Структура - Новая структура с запрошенными полями +// Функция СкопироватьСтруктуру(ИсходнаяСтруктура, КопируемыеКлючи) Результат = Новый Структура(КопируемыеКлючи); diff --git a/tests/HttpBin_API_test.os b/tests/HttpBin_API_test.os index f4c86ff..ce20913 100644 --- a/tests/HttpBin_API_test.os +++ b/tests/HttpBin_API_test.os @@ -1216,7 +1216,7 @@ КонецФункции Функция URL(АдресРсесурса) - Возврат СтрШаблон("%1/%2", HttpBin.URL(), АдресРсесурса); + Возврат HttpBin.URL(АдресРсесурса); КонецФункции Функция ПараметрыКоннектора(Заголовки = Неопределено) @@ -1235,5 +1235,5 @@ КонецФункции Функция Base64ZipФайла() - Возврат "UEsDBAoAAAAAAEAIalpdCci4BwAAAAcAAAALAAAAaHR0cGJpbi50eHRIVFRQQklOUEsBAh8ACgAAAAAAQAhqWl0JyLgHAAAABwAAAAsAJAAAAAAAAAAgAAAAAAAAAGh0dHBiaW4udHh0CgAgAAAAAAABABgAYXX04T6R2wFhdfThPpHbAdjuOJY+kdsBUEsFBgAAAAABAAEAXQAAADAAAAAAAA=="; + Возврат "UEsDBAoAAAAAAEAIalpdCci4BwAAAAcAAAALAAAAaHR0cGJpbi50eHRIVFRQQklOUEsBAh8ACgAAAAAAQAhqWl0JyLgHAAAABwAAAAsAJAAAAAAAAAAgAAAAAAAAAGh0dHBiaW4udHh0CgAgAAAAAAABABgAYXX04T6R2wFhdfThPpHbAdjuOJY+kdsBUEsFBgAAAAABAAEAXQAAADAAAAAAAA=="; // BSLLS:LineLength-off КонецФункции \ No newline at end of file diff --git a/tests/HttpBin_test.os b/tests/HttpBin_test.os index 521c5b1..8cd4da2 100644 --- a/tests/HttpBin_test.os +++ b/tests/HttpBin_test.os @@ -12,36 +12,29 @@ КонецПроцедуры &Тест -Процедура Должен_ЗапуститьСервисВФонеСОжиданиемИОстановить() Экспорт +Процедура ТестДолжен_ЗапуститьСервисСинхронноИОстановить() Экспорт - HttpBin = Новый HttpBin() - .ЗапускатьВФоне() - .ОжидатьЗапуск() - .Запустить(); + HttpBin = Новый HttpBin().Запустить(); Ожидаем.Что(HttpBin.Отвечает(), "Должен быть запущен").ЭтоИстина(); КонецПроцедуры &Тест -Процедура Должен_ЗапуститьСервисВФонеБезОжиданияИОстановить() Экспорт +Процедура ТестДолжен_ЗапуститьСервисАсинхронноИОстановить() Экспорт // Подготовка + HttpBin = Новый HttpBin(); + ВремяНачалаЗапуска = ТекущаяУниверсальнаяДатаВМиллисекундах(); - - HttpBin = Новый HttpBin() - .ЗапускатьВФоне() - .ОжидатьЗапуск() - .Запустить(); - + HttpBin.Запустить(); ВремяЗапуска = ТекущаяУниверсальнаяДатаВМиллисекундах() - ВремяНачалаЗапуска; - HttpBin.Остановить(); Приостановить(200); // Действие - HttpBin.ОжидатьЗапуск(Ложь).Запустить(); + HttpBin.ЗапуститьАсинх(); Приостановить(ВремяЗапуска);