diff --git a/README.md b/README.md index 4bc1bcc..92432e2 100644 --- a/README.md +++ b/README.md @@ -13,10 +13,211 @@ OneScript Package Manager - app - Создать bat-файл для запуска скрипта в указанном каталоге - config - Задать пользовательские настройки - list - Вывести список пакетов +- login - Войти в хаб пакетов и сохранить токен доступа +- push - Отправить пакет в хаб пакетов - help - Справка по командам Наберите *opm <команда> --help*, чтобы вывести справку по команде +# Адрес хаба прямо в команде + +Установка принимает адрес хаба вместе с именем пакета, публикация — опцией `--hub`; +запись в `opm.cfg` для этого не нужна: + +```sh +opm install localhost:3380/corp/my-package@stable # хост, порт, пул, пакет, тег +opm install localhost:3380/asserts # пакет в пуле хаба по умолчанию +opm install https://hub.example.com/public/asserts # схема указана явно +opm push --hub localhost:3380/corp # публикация в пул +opm push --hub localhost:3380 my-package-1.0.0.ospx # публикация в пул по умолчанию +opm push --hub https://hub.example.com/corp +``` + +Формат адреса: `[схема://]хост[:порт][/пул[/пакет[@спецификация]]]`. + +* **Хост без пула — полноценный адрес**, он означает пул хаба по умолчанию. На установке + единственный сегмент после хоста читается как имя пакета, два сегмента — как пул и пакет. +* **Схема.** Если её не указать, берётся `http` для локального хоста (`localhost`, `127.0.0.*`, + `*.localhost`, `[::1]`) и для адреса с явным портом, иначе `https`. Публичное имя без порта + не понижается до открытого канала: по нему уходит токен. +* **После `@`** стоит спецификация — точный номер версии, имя тега либо выражение версии + (см. раздел ниже). На путь запроса она не влияет: при любой форме адреса спецификацию + разрешает хаб по параметру `spec`. +* **Адрес закрепляется только за названным пакетом.** Его зависимости разрешаются + по всем описанным серверам: в чужом пуле их может не быть. +* Токен для адреса берётся из `opm.cfg` по совпадению хоста и порта — его туда пишет `opm login`. +* На установке адрес отличается от имени пакета по виду строки: адресом её делают схема, + форма `хост:порт` либо косая черта при первом сегменте, похожем на хост. Поэтому голое + `hub.example.com` без порта — имя пакета: для такого хаба укажите схему + (`https://hub.example.com`) либо косую черту в конце (`hub.example.com/`). + +**Куда публикует `opm push` без `--hub`.** В описанный в `opm.cfg` сервер — первый, у которого +есть маршрут публикации; его туда пишет `opm login`. Если в настройках публиковать некуда, +пакет уезжает в `hub.oscript.io`. Выбранный хаб называется в выводе команды. Позиционный +аргумент у `push` всегда файл пакета: `opm push localhost:3380/corp` не публикация, а понятный +отказ с подсказкой про `--hub`. + +**Повторная публикация того же номера.** Хаб может отклонить пуш версии, которая в нём уже +лежит: опубликованный номер — то, на что ссылаются чужие зависимости. Какие номера защищены, +решает сам хаб; перезапись заявляется явно: + +```sh +opm push --hub localhost:3380/corp --force +``` + +`--force` — флаг, значения он не требует. С ним к запросу публикации добавляется параметр +`force=1`, а решение о перезаписи остаётся за хабом. Хаб, который такого параметра не знает, +пропустит его молча — публикация в него останется прежней. + +Опции `-m|--mirror` и `-c|--channel` продолжают работать, но помечены устаревшими и печатают +предупреждение. `-c` принимает только имя канала (`stable`, `dev`); адрес пула указывается +опцией `--hub`. + +# Версия, тег или диапазон после `@` + +`install`, `update` и `download` принимают спецификацию после `@` — и в имени пакета, +и в адресе хаба. Спецификацией может быть точный номер версии (`1.0.0`), имя тега +(`stable`, `dev`, `lts`) либо выражение версии (`^1.4`, `~1.4`, `>=1.0`): + +```sh +opm install vrunner@1.0.0 # точная версия +opm install vrunner@stable # тег: версию под ним выбирает хаб +opm install vrunner@^1.4 # диапазон: старшая подходящая версия +opm install vrunner # версию выбирает хаб +``` + +Разрешает спецификацию **хаб**: она уезжает отдельным параметром запроса +`?spec=<спецификация>`, а имя артефакта в адресе остаётся прежним — `<пакет>-<спец>.ospx`. + +**Хаб, который параметра `spec` не принимает, тег и диапазон не разрешает — и это видно.** +Неизвестный параметр такой хаб игнорирует, поэтому файла с именем `<пакет>-stable.ospx` +у него не находится и он отвечает 404, а `opm` объясняет, что нужен хаб, принимающий `spec`. +Точная версия ставится с любым хабом как раньше. Тихой подмены запрошенной версии не происходит нигде: +файловое зеркало (`file://`) по той же причине не подбирает старшую версию каталога, +если спецификация названа. + +# Ошибки связи + +Когда хаб назван в команде явно, недоступность этого хаба — исход команды, а не повод +искать пакет где-то ещё: + +```console +$ opm install lacalhost:3380/contrib/legacy-forms +Не удалось подключиться к http://lacalhost:3380: имя хоста не разрешается +``` + +Различаются неразрешимое имя, отказ в соединении, таймаут и сбой TLS. При переборе нескольких +зеркал из `opm.cfg` недоступное зеркало остаётся предупреждением, и установка идёт дальше. +Служебная обёртка платформы (имя модуля, номер строки, тип .NET-исключения) в сообщение +не попадает — она уходит в отладочный журнал `oscript.app.opm`. + +# Вход в хаб + +```sh +opm login localhost:3380 # спросит: ввести токен или войти через браузер +opm login --browser localhost:3380/corp # вход через браузер (device authorization grant) +opm login --token-stdin localhost:3380 # токен из стандартного ввода, мимо истории команд +opm login --logout localhost:3380 # убрать сохранённый вход +``` + +Данные входа пишутся в домашний файл настроек (`~/.opm.cfg`) с правами только для владельца. + +При входе через браузер клиент печатает **полную ссылку с уже подставленным кодом** и следом +сам код отдельной строкой — на случай, когда терминал рвёт длинный адрес или ссылка не кликается. +Браузер открывается командой своей ОС: `open` на macOS, `xdg-open` в прочих Unix, `start` +на Windows; если открыть не удалось, opm называет причину и предлагает перейти +по напечатанной ссылке вручную. + +Адрес страницы подтверждения приходит **от хаба** и попадает в командную строку, поэтому +открывается только `http(s)`: негодный адрес opm **не печатает и не открывает** — называет +причину и предлагает войти токеном. Перед запуском адрес приводится к URL-кодировке (пробелы, +кавычки и прочие метасимволы кодируются, законные `%`-последовательности вроде `%2F` и `%D0%BF` +не трогаются) и уходит **отдельным параметром**, без командного процессора системы; +на Windows `start` исполняется через `cmd`, но получает уже закодированный адрес. + +**Внимание:** opm ищет файл настроек в порядке «текущий каталог → домашний каталог → системный → +каталог сценария» и берёт **первый найденный, не сливая их**. Если в текущем каталоге лежит +свой `opm.cfg`, он полностью перекроет домашний — `opm login` в этом случае предупреждает. + +# Доверенная публикация из CI + +Конвейеру сборки не нужен долгоживущий токен в секретах: он предъявляет хабу короткоживущий +**OIDC id-token** своего провайдера, а хаб сверяет его с записью доверия. Токен клиент получает +**сам** — писать в workflow шаг «сходить за токеном и подставить его в opm» не нужно. + +**Со стороны хаба.** На странице пакета, раздел «Доверенная публикация», заводится запись: +провайдер (GitHub Actions либо GitLab CI), репозиторий (`владелец/репозиторий`) и, если нужно +сузить доверие, путь конвейера и ref ветки или тега; пустые конвейер и ref означают «любые». +Доверие настраивается на **существующем** пакете, поэтому занять им свободное имя нельзя. + +**Со стороны конвейера.** Отдельных опций у доверенной публикации нет: её включает набор +переменных среды. GitHub Actions — достаточно разрешения `id-token: write`: + +```yaml +name: publish +on: + push: + tags: ['*'] +permissions: + contents: read + id-token: write +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: otymko/setup-onescript@v1.4 + - run: opm install -l && opm build . + - run: opm push --hub https://hub.example.com/corp + # аудитория по умолчанию — адрес хаба; другой адрес называют явно: + # - run: opm push --hub https://hub.example.com/corp --oidc-audience https://openhub.example.com +``` + +GitLab CI выдаёт id-token переменной среды, **имя которой назначаете вы** секцией `id_tokens`. +Назовите её `id_token` — тогда opm найдёт её сам: + +```yaml +publish: + id_tokens: + id_token: + aud: https://hub.example.com + script: + - opm install -l && opm build . + - opm push --hub https://hub.example.com/corp +``` + +Переменная называется иначе — назовите её клиенту опцией `--oidc-token-var` +(либо `OPM_OIDC_TOKEN_VAR`): `opm push --oidc-token-var OPENHUB_TOKEN --hub https://hub.example.com/corp`. + +* **Решают переменные среды.** Нашлась переменная `id_token` (либо названная + `--oidc-token-var`), а в GitHub Actions — доступ к сервису токенов: opm пишет в вывод, + почему публикует доверенно, и идёт этим путём. Не нашлось — причина уходит в отладочный + журнал, а публикация идёт токеном: `--token`, `--token-stdin`, затем сохранённый вход + из `~/.opm.cfg`. Нет и токена — команда отказывает, назвав обе причины. +* **Доверенная публикация главнее токена в команде.** Раз конвейер выдал id-token, публикуют им. +* **Названная `--oidc-token-var` — заявленное намерение.** Если переменной нет или она пуста, + opm объяснит про секцию `id_tokens`, а не откатится молча на токен. +* **`audience` обязателен и по умолчанию равен адресу хаба, в который публикуем** — хаб сверяет + клейм `aud` со своим адресом и отказывает при несовпадении. Переопределяется `--oidc-audience` + (либо `OPM_OIDC_AUDIENCE`); в GitLab то же значение ставится в `aud` секции `id_tokens`. +* Подпись id-token проверяет **хаб**: клиент только получает значение и отдаёт его в заголовке. +* **Хаб доверия не принял** — opm покажет код ответа и сообщение хаба с пометкой, что не удалась + именно доверенная публикация: поддержку доверия и запись доверия ведёт хаб, клиенту видно + только его решение. +* Не хватило `permissions: id-token: write` — opm так и скажет, а не покажет голый код ответа. + +# Проверка контрольной суммы + +Скачанный файл сверяется с контрольной суммой хаба (`X-Checksum-Sha256`, иначе `ETag`): + +* по умолчанию — **мягкий режим**: заголовок есть — сверяем и падаем при расхождении; + заголовка нет (старый хаб) — ставим как раньше; +* `--verify-checksum` — **строгий режим**: отсутствие контрольной суммы тоже ошибка; +* `--no-verify-checksum` — не сверять вовсе. + +Настройка `ПроверкаКонтрольнойСуммы` в `opm.cfg` принимает те же значения: +`выкл`, `мягкая`, `строгая`. + Справка из википедии: [Система управления пакетами](https://ru.wikipedia.org/wiki/%D0%A1%D0%B8%D1%81%D1%82%D0%B5%D0%BC%D0%B0_%D1%83%D0%BF%D1%80%D0%B0%D0%B2%D0%BB%D0%B5%D0%BD%D0%B8%D1%8F_%D0%BF%D0%B0%D0%BA%D0%B5%D1%82%D0%B0%D0%BC%D0%B8) # Сервера diff --git a/packagedef b/packagedef index 92e5b8f..24aa0e6 100644 --- a/packagedef +++ b/packagedef @@ -13,7 +13,7 @@ Константы_ЛокальнаяВерсия = ЗагрузитьСценарий(ПутьКСценариюКонстант); ВерсияПродукта = Константы_ЛокальнаяВерсия.ВерсияПродукта; Иначе - ВерсияПродукта = "1.6.6"; + ВерсияПродукта = "1.7.0"; КонецЕсли; Описание.Имя("opm") @@ -28,6 +28,7 @@ .ЗависитОт("gitrunner", "1.7.1") .ЗависитОт("reflector", "0.7.1") .ЗависитОт("semver", "1.0.0") + .ЗависитОт("1commands", "1.3.1") // Зависимости для разработки, РазработкаЗависитОт() нельзя использовать // т.к иначе не получится обновится из старой версии opm @@ -39,6 +40,13 @@ .ВключитьФайл("src") .ВключитьФайл("tasks") .ВключитьФайл("oscript_modules") + .ОпределяетКласс("ВходВХаб", "src/core/Классы/ВходВХаб.os") + .ОпределяетКласс("ОкружениеПроцесса", "src/core/Классы/ОкружениеПроцесса.os") + .ОпределяетКласс("ЗапускательПрограмм", "src/core/Классы/ЗапускательПрограмм.os") + .ОпределяетКласс("ОткрывательБраузера", "src/core/Классы/ОткрывательБраузера.os") + .ОпределяетКласс("ЗапросчикHTTP", "src/core/Классы/ЗапросчикHTTP.os") + .ОпределяетКласс("ТокенКонвейераCI", "src/core/Классы/ТокенКонвейераCI.os") + .ОпределяетКласс("ОтправительПакета", "src/core/Классы/ОтправительПакета.os") .ОпределяетКласс("КэшУстановленныхПакетов", "src/core/Классы/КэшУстановленныхПакетов.os") .ОпределяетКласс("МенеджерПолученияПакетов", "src/core/Классы/МенеджерПолученияПакетов.os") .ОпределяетКласс("МенеджерУстановкиПакетов", "src/core/Классы/МенеджерУстановкиПакетов.os") @@ -47,6 +55,10 @@ .ОпределяетКласс("СерверПакетов", "src/core/Классы/СерверПакетов.os") .ОпределяетКласс("СериализацияМетаданныхПакета", "src/core/Классы/СериализацияМетаданныхПакета.os") .ОпределяетКласс("УстановкаПакета", "src/core/Классы/УстановкаПакета.os") + .ОпределяетМодуль("АдресаХаба", "src/core/Модули/АдресаХаба.os") + .ОпределяетМодуль("КонтрольныеСуммы", "src/core/Модули/КонтрольныеСуммы.os") + .ОпределяетМодуль("ОшибкиСети", "src/core/Модули/ОшибкиСети.os") + .ОпределяетМодуль("ФайлыНастроекOpm", "src/core/Модули/ФайлыНастроекOpm.os") .ОпределяетМодуль("КонстантыOpm", "src/core/Модули/КонстантыOpm.os") .ОпределяетМодуль("НастройкиOpm", "src/core/Модули/НастройкиOpm.os") .ОпределяетМодуль("РаботаСВерсиями", "src/core/Модули/РаботаСВерсиями.os") diff --git a/src/cmd/opm.os b/src/cmd/opm.os index 37ed044..eeda44e 100644 --- a/src/cmd/opm.os +++ b/src/cmd/opm.os @@ -11,6 +11,7 @@ #Использовать "../core" #Использовать "." #Использовать cli +#Использовать logos Перем ЭтоWindows; @@ -26,14 +27,15 @@ Приложение.ДобавитьКоманду("a app", НСтр("ru='Создать ';en='Create '") + ?(ЭтоWindows, "bat", "sh") + НСтр("ru='-файл для запуска скрипта в указанном каталоге';en='-file to run the script in the specified directory'"), Новый КомандаOpm_App); Приложение.ДобавитьКоманду("b build", НСтр("ru='Собрать пакет из исходников';en='Build a package from source'"), Новый КомандаOpm_Build); Приложение.ДобавитьКоманду("c config", НСтр("ru='Задать пользовательские настройки';en='Configure user settings'"), Новый КомандаOpm_Config); - Приложение.ДобавитьКоманду("i install", НСтр("ru='Выполнить установку пакета. - | Если указано имя пакета, происходит установка из хаба или из файла. + Приложение.ДобавитьКоманду("i install", НСтр("ru='Выполнить установку пакета. + | Если указано имя пакета, происходит установка из хаба или из файла. | В обратном случае устанавливаются зависимости текущего пакета по файлу packagedef.' |en='Install the package. - | If the package name is specified, the installation is performed from the hub or from the file. + | If the package name is specified, the installation is performed from the hub or from the file. | Otherwise, the dependencies of the current package are set using the packagedef file.'"), Новый КомандаOpm_Install); Приложение.ДобавитьКоманду("ls list", НСтр("ru='Вывести список пакетов';en='Show list of packages'"), Новый КомандаOpm_List); + Приложение.ДобавитьКоманду("login", НСтр("ru='Войти в хаб пакетов и сохранить токен доступа';en='Log in to the package hub and store the access token'"), Новый КомандаOpm_Login); Приложение.ДобавитьКоманду("pre prepare", НСтр("ru='Подготовить новый каталог разрабатываемого пакета';en='Prepare a new catalog for the development package'"), Новый КомандаOpm_Prepare); Приложение.ДобавитьКоманду("p push", НСтр("ru='Отправить пакет в хаб пакетов';en='Send the package to the package hub'"), Новый КомандаOpm_Push); Приложение.ДобавитьКоманду("r run", НСтр("ru='Выполнить произвольную задачу';en='Run a task'"), Новый КомандаOpm_Run); @@ -44,7 +46,7 @@ НСтр("ru='Скачать файл пакета и файлы зависимостей'; |en='Download package and dependencies'"), Новый КомандаOpm_Download); - + Приложение.Запустить(АргументыКоманднойСтроки); КонецПроцедуры @@ -63,7 +65,10 @@ ЗавершитьРаботу(0); Исключение - Сообщить(ОписаниеОшибки()); + // человеку — суть, служебная обёртка с модулем и номером строки уходит в отладочный журнал + ПодробноеОписание = ОписаниеОшибки(); + Логирование.ПолучитьЛог(КонстантыOpm.ИмяЛога).Отладка(ПодробноеОписание); + Сообщить(ОшибкиСети.Суть(ПодробноеОписание)); ВременныеФайлы.Удалить(); 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\260Opm_Download.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\260Opm_Download.os" index 57e0253..4cd53e5 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\260Opm_Download.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\260Opm_Download.os" @@ -11,8 +11,9 @@ КомандаПриложения.Опция("s skip-install-deps", Ложь, "признак пропуска установки зависимых пакетов"); КомандаПриложения.Опция("d dest", "", "Целевой каталог"); - ОпцияЗеркала = КомандаПриложения.Опция("m mirror", "", "Указать имя сервера, с которого необходимо ставить пакеты. - | Доступные сервера прописываются в конфигурационном файле opm.cfg, параметр 'СервераПакетов'.") + ОпцияЗеркала = КомандаПриложения.Опция("m mirror", "", "УСТАРЕЛА. Указать имя сервера, с которого необходимо ставить пакеты. + | Доступные сервера прописываются в конфигурационном файле opm.cfg, параметр 'СервераПакетов'. + | Вместо неё указывайте адрес хаба прямо в аргументе: хост[:порт]/пул/пакет[@спецификация].") .ВОкружении("OPM_HUB_MIRROR") .ТПеречисление(); @@ -21,7 +22,12 @@ ОпцияЗеркала.Перечисление(ДоступноеЗеркало, ДоступноеЗеркало, "Сервер '" + ДоступноеЗеркало + "'"); КонецЦикла; - КомандаПриложения.Аргумент("PACKAGE", "", "Имя пакета в хабе. Чтобы установить конкретную версию, используйте ИмяПакета@ВерсияПакета") + ПараметрыПриложенияOpm.ОписатьОпцииПроверкиСуммы(КомандаПриложения); + + КомандаПриложения.Аргумент("PACKAGE", "", "Имя пакета в хабе. После @ можно указать спецификацию: + | точную версию (пакет@1.0.0), имя тега (пакет@stable) или выражение версии (пакет@^1.4). + | Тег и выражение разрешает хаб; хаб, не принимающий параметр spec, отвечает отказом. + | Можно указать адрес хаба целиком: [схема://]хост[:порт]/пул/пакет[@спецификация].") .ТМассивСтрок() .Обязательный(Ложь); @@ -58,6 +64,9 @@ Лог.Отладка("МассивПакетовКУстановке: %1", МассивПакетовКУстановке.Количество()); Лог.Отладка("НеобходимоУстановитьЗависимости: %1", НеобходимоУстановитьЗависимости); + ПараметрыПриложенияOpm.ПрименитьОпцииПроверкиСуммы(КомандаПриложения); + ПараметрыПриложенияOpm.ПредупредитьОбУстаревшемЗеркале(ИмяСервера); + НастройкаУстановки = РаботаСПакетами.ПолучитьНастройкуУстановки(); НастройкаУстановки.УстанавливатьЗависимости = НеобходимоУстановитьЗависимости; НастройкаУстановки.УстанавливатьЗависимостиРазработчика = УстановкаПакетовРазработчика; @@ -115,15 +124,19 @@ Для каждого ИмяПакета Из МассивПакетовКУстановке Цикл Если ЭтоФайлПакета(ИмяПакета) Тогда - + РаботаСПакетами.УстановитьПакетИзФайла(ИмяПакета, РежимУстановки, ЦелевойКаталог, НастройкаУстановки); - + + ИначеЕсли АдресаХаба.ЭтоАдресХаба(ИмяПакета) Тогда + + РаботаСПакетами.УстановитьПакетПоАдресу(ИмяПакета, РежимУстановки, ЦелевойКаталог, НастройкаУстановки); + Иначе РаботаСПакетами.УстановитьПакетИзОблака(ИмяПакета, РежимУстановки, ЦелевойКаталог, НастройкаУстановки); - + КонецЕсли; - + КонецЦикла; КонецЕсли; 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\260Opm_Install.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\260Opm_Install.os" index dad2baf..b4309b8 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\260Opm_Install.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\260Opm_Install.os" @@ -3,7 +3,6 @@ Процедура ОписаниеКоманды(Знач КомандаПриложения) Экспорт - КомандаПриложения.Опция("a all", Ложь, "Установить все пакеты, зарегистрированные в хабе"); КомандаПриложения.Опция("f file", "", "Указать файл из которого нужно установить пакет. Поддерживает указание маски файла для пакетной установки"); КомандаПриложения.Опция("u url", "", "Указать прямую интернет-ссылку на файл *.ospx из которого нужно установить пакет"); @@ -13,8 +12,9 @@ КомандаПриложения.Опция("skip-create-app", Ложь, "признак отключения создания файла запуска"); КомандаПриложения.Опция("d dest", "", "Переопределить стандартный каталог в который устанавливаются пакеты (вместо oscript_modules)"); - ОпцияЗеркала = КомандаПриложения.Опция("m mirror", "", "Указать имя сервера, с которого необходимо ставить пакеты. - | Доступные сервера прописываются в конфигурационном файле opm.cfg, параметр 'СервераПакетов'.") + ОпцияЗеркала = КомандаПриложения.Опция("m mirror", "", "УСТАРЕЛА. Указать имя сервера, с которого необходимо ставить пакеты. + | Доступные сервера прописываются в конфигурационном файле opm.cfg, параметр 'СервераПакетов'. + | Вместо неё указывайте адрес хаба прямо в аргументе: хост[:порт]/пул/пакет[@спецификация].") .ВОкружении("OPM_HUB_MIRROR") .ТПеречисление(); @@ -23,11 +23,16 @@ ОпцияЗеркала.Перечисление(ДоступноеЗеркало, ДоступноеЗеркало, "Сервер '" + ДоступноеЗеркало + "'"); КонецЦикла; - КомандаПриложения.Аргумент("PACKAGE", "", "Имя пакета в хабе. Чтобы установить конкретную версию, используйте ИмяПакета@ВерсияПакета") + ПараметрыПриложенияOpm.ОписатьОпцииПроверкиСуммы(КомандаПриложения); + + КомандаПриложения.Аргумент("PACKAGE", "", "Имя пакета в хабе. После @ можно указать спецификацию: + | точную версию (пакет@1.0.0), имя тега (пакет@stable) или выражение версии (пакет@^1.4). + | Тег и выражение разрешает хаб; хаб, не принимающий параметр spec, отвечает отказом. + | Можно указать адрес хаба целиком: [схема://]хост[:порт]/пул/пакет[@спецификация].") .ТМассивСтрок() .Обязательный(Ложь); - - // КомандаПриложения.Спек = "(-a | --all | -l | --local | -d | --dest )"; + + // КомандаПриложения.Спек = "(-l | --local | -d | --dest )"; КонецПроцедуры @@ -35,7 +40,6 @@ УстановкаВЛокальныйКаталог = КомандаПриложения.ЗначениеОпции("local"); УстановкаПакетовРазработчика = КомандаПриложения.ЗначениеОпции("dev"); - УстановкаВсехПакетов = КомандаПриложения.ЗначениеОпции("all"); КаталогУстановки = КомандаПриложения.ЗначениеОпции("dest"); ФайлПакетаУстановки = КомандаПриложения.ЗначениеОпции("file"); ИнтернетСсылкаНаПакет = КомандаПриложения.ЗначениеОпции("url"); @@ -64,7 +68,6 @@ Лог.Отладка("УстановкаВЛокальныйКаталог: %1", УстановкаВЛокальныйКаталог); Лог.Отладка("УстановкаПакетовРазработчика: %1", УстановкаПакетовРазработчика); - Лог.Отладка("УстановкаВсехПакетов: %1", УстановкаВсехПакетов); Лог.Отладка("КаталогУстановки: %1", КаталогУстановки); Лог.Отладка("ФайлПакетаУстановки: %1", ФайлПакетаУстановки); Лог.Отладка("ИнтернетСсылкаНаПакет: %1", ИнтернетСсылкаНаПакет); @@ -72,15 +75,16 @@ Лог.Отладка("НеобходимоУстановитьЗависимости: %1", НеобходимоУстановитьЗависимости); Лог.Отладка("СоздаватьФайлыЗапуска: %1", СоздаватьФайлыЗапуска); + ПараметрыПриложенияOpm.ПрименитьОпцииПроверкиСуммы(КомандаПриложения); + ПараметрыПриложенияOpm.ПредупредитьОбУстаревшемЗеркале(ИмяСервера); + НастройкаУстановки = РаботаСПакетами.ПолучитьНастройкуУстановки(); НастройкаУстановки.УстанавливатьЗависимости = НеобходимоУстановитьЗависимости; НастройкаУстановки.УстанавливатьЗависимостиРазработчика = УстановкаПакетовРазработчика; НастройкаУстановки.СоздаватьФайлыЗапуска = СоздаватьФайлыЗапуска; НастройкаУстановки.ИмяСервера = ИмяСервера; - Если УстановкаВсехПакетов Тогда - РаботаСПакетами.УстановитьВсеПакетыИзОблака(РежимУстановки, ЦелевойКаталог, НастройкаУстановки); - ИначеЕсли ПустаяСтрока(ФайлПакетаУстановки) И ПустаяСтрока(ИнтернетСсылкаНаПакет) И МассивПакетовКУстановке.Количество() = 0 Тогда + Если ПустаяСтрока(ФайлПакетаУстановки) И ПустаяСтрока(ИнтернетСсылкаНаПакет) И МассивПакетовКУстановке.Количество() = 0 Тогда РаботаСПакетами.УстановитьПакетыПоОписаниюПакета(РежимУстановки, ЦелевойКаталог, НастройкаУстановки); ИначеЕсли НЕ ПустаяСтрока(ФайлПакетаУстановки) Тогда @@ -129,19 +133,23 @@ Для каждого ИмяПакета Из МассивПакетовКУстановке Цикл Если ЭтоФайлПакета(ИмяПакета) Тогда - + РаботаСПакетами.УстановитьПакетИзФайла(ИмяПакета, РежимУстановки, ЦелевойКаталог, НастройкаУстановки); - + + ИначеЕсли АдресаХаба.ЭтоАдресХаба(ИмяПакета) Тогда + + РаботаСПакетами.УстановитьПакетПоАдресу(ИмяПакета, РежимУстановки, ЦелевойКаталог, НастройкаУстановки); + Иначе РаботаСПакетами.УстановитьПакетИзОблака(ИмяПакета, РежимУстановки, ЦелевойКаталог, НастройкаУстановки); - + КонецЕсли; - + КонецЦикла; КонецЕсли; - + КонецПроцедуры Функция ЭтоФайлПакета(Знач ИмяПакета) 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\260Opm_Login.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\260Opm_Login.os" new file mode 100644 index 0000000..cc1ec0e --- /dev/null +++ "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\260Opm_Login.os" @@ -0,0 +1,280 @@ +#Использовать logos +#Использовать 1commands + +Перем Лог; + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Прикладной интерфейс + +Процедура ОписаниеКоманды(Знач КомандаПриложения) Экспорт + + КомандаПриложения.Опция("t token", "", "Токен доступа. Попадает в историю команд — в скриптах лучше " + + "передавать его через --token-stdin или переменную среды.") + .ВОкружении("OPM_HUB_TOKEN"); + КомандаПриложения.Опция("token-stdin", Ложь, "Прочитать токен из стандартного ввода."); + КомандаПриложения.Опция("b browser", Ложь, "Войти через браузер: хаб покажет код подтверждения, " + + "человек одобряет вход в кабинете."); + КомандаПриложения.Опция("no-browser", Ложь, "Не открывать браузер автоматически, только показать адрес и код."); + КомандаПриложения.Опция("scopes", "read,write", "Запрашиваемые права токена при входе через браузер."); + КомандаПриложения.Опция("logout", Ложь, "Удалить сохранённый вход по этому адресу."); + + КомандаПриложения.Аргумент("HUB", "", "Адрес хаба: [схема://]хост[:порт][/пул].") + .Обязательный(Истина); + +КонецПроцедуры + +Процедура ВыполнитьКоманду(Знач КомандаПриложения) Экспорт + + Адрес = КомандаПриложения.ЗначениеАргумента("HUB"); + РазобранныйАдрес = АдресаХаба.Разобрать(Адрес); + + Если ЗначениеЗаполнено(РазобранныйАдрес.ИмяПакета) Тогда + ВызватьИсключение СтрШаблон( + "Адрес входа <%1>: ожидался адрес хаба или пула, а не пакета", Адрес); + КонецЕсли; + + Если КомандаПриложения.ЗначениеОпции("logout") Тогда + ВыполнитьВыход(РазобранныйАдрес); + Возврат; + КонецЕсли; + + АдресаХаба.ПредупредитьОбОткрытомКанале(РазобранныйАдрес); + + Токен = ПолучитьТокен(КомандаПриложения, РазобранныйАдрес); + + Если ПустаяСтрока(Токен) Тогда + ВызватьИсключение "Токен не получен — вход не выполнен"; + КонецЕсли; + + Вход = Новый ВходВХаб(РазобранныйАдрес); + Проверка = Вход.ПроверитьТокен(Токен); + + Если НЕ Проверка.Успех Тогда + Если Проверка.Код = 0 Тогда + ВызватьИсключение Проверка.Сообщение; + КонецЕсли; + ВызватьИсключение СтрШаблон("Хаб %1 не принял токен: %2", + РазобранныйАдрес.БазовыйАдрес, Проверка.Сообщение); + КонецЕсли; + + СохранитьВход(РазобранныйАдрес, Токен, Проверка.Логин); + +КонецПроцедуры + +// Приглашение к подтверждению входа: полный адрес с подставленным кодом, затем код +// отдельной строкой — на случай, когда терминал рвёт длинную ссылку или она не кликается. +// +// Параметры: +// АдресПерехода - Строка - адрес страницы подтверждения. +// КодПользователя - Строка - код подтверждения. +// +// Возвращаемое значение: +// Массив из Строка - строки для вывода пользователю. +// +Функция СтрокиПриглашения(Знач АдресПерехода, Знач КодПользователя) Экспорт + + Строки = Новый Массив(); + Строки.Добавить(СтрШаблон("Откройте в браузере: %1", АдресПерехода)); + Строки.Добавить(СтрШаблон("Код подтверждения: %1", КодПользователя)); + + Возврат Строки; + +КонецФункции + +/////////////////////////////////////////////////////////////////////////////////////////////////// + +Функция ПолучитьТокен(Знач КомандаПриложения, Знач РазобранныйАдрес) + + Если КомандаПриложения.ЗначениеОпции("token-stdin") Тогда + Возврат СокрЛП(ПрочитатьСтрокуВвода()); + КонецЕсли; + + ЗначениеОпцииТокена = СокрЛП(КомандаПриложения.ЗначениеОпции("token")); + + Если НЕ ПустаяСтрока(ЗначениеОпцииТокена) Тогда + Возврат ЗначениеОпцииТокена; + КонецЕсли; + + Если КомандаПриложения.ЗначениеОпции("browser") Тогда + Возврат ВойтиЧерезБраузер(КомандаПриложения, РазобранныйАдрес); + КонецЕсли; + + Возврат ЗапроситьТокенУПользователя(КомандаПриложения, РазобранныйАдрес); + +КонецФункции + +Функция ЗапроситьТокенУПользователя(Знач КомандаПриложения, Знач РазобранныйАдрес) + + Сообщить(СтрШаблон("Вход в хаб %1", РазобранныйАдрес.БазовыйАдрес)); + Сообщить(" 1 — ввести токен доступа вручную"); + Сообщить(" 2 — войти через браузер"); + + Выбор = СокрЛП(ПрочитатьСтрокуВвода("Выбор [1]: ")); + + Если Выбор = "2" Тогда + Возврат ВойтиЧерезБраузер(КомандаПриложения, РазобранныйАдрес); + КонецЕсли; + + Возврат ПрочитатьТокенБезЭха(); + +КонецФункции + +Функция ВойтиЧерезБраузер(Знач КомандаПриложения, Знач РазобранныйАдрес) + + Вход = Новый ВходВХаб(РазобранныйАдрес); + + Заявка = Вход.НачатьВходЧерезБраузер(ИмяКлиента(), КомандаПриложения.ЗначениеОпции("scopes")); + + Если НЕ Заявка.Успех Тогда + // код 0 — до хаба не дозвонились; это отдельная беда, а не отсутствие поддержки потока + Если Заявка.Код = 0 Тогда + ВызватьИсключение Заявка.Сообщение; + КонецЕсли; + ВызватьИсключение СтрШаблон( + "Хаб %1 не поддерживает вход через браузер (%2). Выпустите токен в кабинете и передайте его: " + + "opm login %1 --token-stdin", РазобранныйАдрес.БазовыйАдрес, Заявка.Сообщение); + КонецЕсли; + + АдресПерехода = Вход.АдресПодтвержденияДляЧеловека(Заявка); + Открыватель = Новый ОткрывательБраузера(); + + // негодный адрес не печатаем вовсе: предложить перейти по нему вручную значит отдать + // человеку ровно то, что мы только что признали небезопасным + ОтказВАдресе = Открыватель.ПричинаОтказаВАдресе(АдресПерехода); + Если НЕ ПустаяСтрока(ОтказВАдресе) Тогда + ВызватьИсключение СтрШаблон("%1. Адрес подтверждения пришёл от хаба %2 — доверять ему" + + " не стоит. Выпустите токен в кабинете и передайте его: opm login %2 --token-stdin", + ОтказВАдресе, РазобранныйАдрес.БазовыйАдрес); + КонецЕсли; + + Сообщить(""); + Для Каждого Строка_ Из СтрокиПриглашения(АдресПерехода, Заявка.КодПользователя) Цикл + Сообщить(Строка_); + КонецЦикла; + Сообщить(""); + + Если НЕ КомандаПриложения.ЗначениеОпции("no-browser") Тогда + Открытие = Открыватель.Открыть(АдресПерехода); + Если Открытие.Успех Тогда + Сообщить("Браузер открыт. Подтвердите вход и вернитесь сюда."); + Иначе + Сообщить(СтрШаблон("Открыть браузер не удалось: %1", Открытие.Сообщение)); + Сообщить("Перейдите по адресу выше вручную — код в нём уже подставлен."); + КонецЕсли; + КонецЕсли; + + Сообщить("Жду подтверждения…"); + + Результат = Вход.ДождатьсяПодтверждения(Заявка.КодУстройства, Заявка.СрокСекунд, Заявка.ИнтервалСекунд); + + Если НЕ Результат.Успех Тогда + ВызватьИсключение СтрШаблон("Вход через браузер не выполнен: %1", Результат.Сообщение); + КонецЕсли; + + Возврат Результат.Токен; + +КонецФункции + +Процедура СохранитьВход(Знач РазобранныйАдрес, Знач Токен, Знач Логин) + + ПутьДомашнего = ФайлыНастроекOpm.ПутьДомашнегоФайла(); + + Настройки = ФайлыНастроекOpm.ПрочитатьНастройки(ПутьДомашнего); + ИмяСервера = ФайлыНастроекOpm.ЗаписатьСерверПакетов(Настройки, РазобранныйАдрес, Токен); + ФайлыНастроекOpm.ЗаписатьНастройки(Настройки, ПутьДомашнего); + + Сообщить(СтрШаблон("Вход выполнен%1: %2 сохранён в %3 как сервер <%4>", + ?(ПустаяСтрока(Логин), "", " как " + Логин), + АдресаХаба.ИмяСервераДляАдреса(РазобранныйАдрес), ПутьДомашнего, ИмяСервера)); + + ПредупредитьОПерекрытииКонфига(ПутьДомашнего); + +КонецПроцедуры + +Процедура ВыполнитьВыход(Знач РазобранныйАдрес) + + ПутьДомашнего = ФайлыНастроекOpm.ПутьДомашнегоФайла(); + + Настройки = ФайлыНастроекOpm.ПрочитатьНастройки(ПутьДомашнего); + + Если ФайлыНастроекOpm.УдалитьСерверПакетов(Настройки, РазобранныйАдрес) Тогда + ФайлыНастроекOpm.ЗаписатьНастройки(Настройки, ПутьДомашнего); + Сообщить(СтрШаблон("Вход в %1 удалён из %2", + АдресаХаба.ИмяСервераДляАдреса(РазобранныйАдрес), ПутьДомашнего)); + Иначе + Сообщить(СтрШаблон("Сохранённого входа в %1 не найдено", + АдресаХаба.ИмяСервераДляАдреса(РазобранныйАдрес))); + КонецЕсли; + + ПредупредитьОПерекрытииКонфига(ПутьДомашнего); + +КонецПроцедуры + +// opm читает первый найденный файл настроек и не сливает его с остальными, +// поэтому opm.cfg в текущем каталоге отменяет всё, что записано в домашний. +Процедура ПредупредитьОПерекрытииКонфига(Знач ПутьДомашнего) + + Действующий = ФайлыНастроекOpm.ПутьДействующегоФайла(); + + Если ПустаяСтрока(Действующий) ИЛИ Действующий = ПутьДомашнего Тогда + Возврат; + КонецЕсли; + + Лог.Предупреждение( + "Файл настроек %1 перекрывает %2: opm берёт первый найденный конфиг и не сливает их. " + + "Пока этот файл существует, сохранённый вход не подхватится — перенесите запись сервера в него " + + "или уберите его.", Действующий, ПутьДомашнего); + +КонецПроцедуры + +// Токен вводится без эха, чтобы не остаться на экране и в скроллбэке терминала. +Функция ПрочитатьТокенБезЭха() + + ЭхоВыключено = ПереключитьЭхоТерминала(Ложь); + Токен = СокрЛП(ПрочитатьСтрокуВвода("Токен доступа (ввод скрыт): ")); + Если ЭхоВыключено Тогда + ПереключитьЭхоТерминала(Истина); + Сообщить(""); + КонецЕсли; + + Возврат Токен; + +КонецФункции + +Функция ПереключитьЭхоТерминала(Знач Включить) + + СистемнаяИнформация = Новый СистемнаяИнформация(); + Если Найти(НРег(СистемнаяИнформация.ВерсияОС), "windows") > 0 Тогда + Возврат Ложь; + КонецЕсли; + + Попытка + Команда = Новый Команда(); + Команда.УстановитьКоманду("stty"); + Команда.ДобавитьПараметр(?(Включить, "echo", "-echo")); + Команда.ПоказыватьВыводНемедленно(Ложь); + Возврат Команда.Исполнить() = 0; + Исключение + Возврат Ложь; + КонецПопытки; + +КонецФункции + +Функция ПрочитатьСтрокуВвода(Знач Подсказка = "") + + Значение = ""; + ВвестиСтроку(Значение, Подсказка); + + Возврат ?(Значение = Неопределено, "", Значение); + +КонецФункции + +Функция ИмяКлиента() + + СистемнаяИнформация = Новый СистемнаяИнформация(); + + Возврат СтрШаблон("opm %1 на %2", КонстантыOpm.ВерсияПродукта, СистемнаяИнформация.ИмяКомпьютера); + +КонецФункции + +Лог = Логирование.ПолучитьЛог("oscript.app.opm.login"); 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\260Opm_Push.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\260Opm_Push.os" index 0603d2a..2f1e4f2 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\260Opm_Push.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\260Opm_Push.os" @@ -1,4 +1,3 @@ -#Использовать asserts #Использовать logos #Использовать gitrunner @@ -9,24 +8,29 @@ // Прикладной интерфейс Процедура ОписаниеКоманды(Знач КомандаПриложения) Экспорт - - КомандаПриложения.Опция("token", "", "Токен авторизации на GitHub.com - | - Токен авторизации создается на странице https://github.com/settings/tokens - | - Токен используется только для проверки авторизации на GitHub.com и прав на репозиторий, + + КомандаПриложения.Опция("hub", "", "Адрес хаба публикации: [схема://]хост[:порт][/пул]. + | Не указан — берётся описанный в opm.cfg сервер, + | а если и его нет — хаб по умолчанию.") + .ВОкружении("OPM_HUB"); + + КомандаПриложения.Опция("token", "", "Токен авторизации в хабе пакетов. + | - Для OpenHub это персональный токен доступа (PAT), выданный opm login. + | - Для hub.oscript.io — токен GitHub со страницы https://github.com/settings/tokens, + | он используется только для проверки прав на репозиторий, | выдача дополнительных разрешений в ""scopes"" НЕ требуется.") .ВОкружении("GITHUB_OAUTH_TOKEN"); ; - КомандаПриложения.Опция("f file", "", "Маска или имя файла пакета."); - КомандаПриложения.Опция("c channel", "auto", "Канал публикации.") - .ТПеречисление() - .Перечисление("auto", "auto", "Автоматическое определение канала. - | В случае отправки из ветки master гит-репозитория данный параметр можно опустить - будет использоваться канал ""stable"". - | В любых других случаях его заполнение обязательно.") - .Перечисление("stable", "stable", "Канал содержащий стабильные версии пакетов") - .Перечисление("dev", "dev" , "Канал содержащий разработческие версии пакетов") + КомандаПриложения.Опция("token-stdin", Ложь, "Прочитать токен из стандартного ввода, мимо истории команд."); + КомандаПриложения.Опция("force", Ложь, "Опубликовать поверх уже занятого номера версии. + | Хаб может отклонить повторную публикацию номера; + | какие номера защищены, решает он сам."); + КомандаПриложения.Опция("f file", "", "УСТАРЕЛА. Маска или имя файла пакета — указывайте аргументом."); + КомандаПриложения.Опция("c channel", "auto", "УСТАРЕЛА: канал версии хаб определяет тегами пакета. + | Принимает имя канала (stable, dev); адрес пула указывается опцией <--hub>.") .ВОкружении("OPM_HUB_CHANNEL"); - ОпцияЗеркала = КомандаПриложения.Опция("m mirror", "СерверУдаленногоХранилища", "Имя сервера для публикации. - | Доступные сервера прописываются в конфигурационном файле opm.cfg, параметр 'СервераПакетов'.") + ОпцияЗеркала = КомандаПриложения.Опция("m mirror", "", "УСТАРЕЛА. Имя сервера для публикации из opm.cfg. + | Вместо неё указывайте адрес пула опцией <--hub>: [схема://]хост[:порт]/пул.") .ВОкружении("OPM_HUB_MIRROR") .ТПеречисление(); @@ -35,79 +39,640 @@ ОпцияЗеркала.Перечисление(ДоступноеЗеркало, ДоступноеЗеркало, "Сервер '" + ДоступноеЗеркало + "'"); КонецЦикла; - КомандаПриложения.Аргумент("FILE", "", "Маска или имя файла пакета.") + ОписатьОпцииДоверия(КомандаПриложения); + + КомандаПриложения.Аргумент("FILE", "", "Файл пакета либо маска файлов. + | По умолчанию — единственный *.ospx в текущем каталоге.") .Обязательный(Ложь); КонецПроцедуры Процедура ВыполнитьКоманду(Знач КомандаПриложения) Экспорт - - ТокенАвторизации = КомандаПриложения.ЗначениеОпции("token"); - МаскаФайлаПакетаСтарая = КомандаПриложения.ЗначениеОпции("file"); - ИмяКаналаПубликации = КомандаПриложения.ЗначениеОпции("channel"); + + МаскаФайлаПакета = МаскаФайлаПакетаИзКоманды(КомандаПриложения); ИмяСервераПакетов = КомандаПриложения.ЗначениеОпции("mirror"); + ИмяКанала = ИмяКаналаИзКоманды(КомандаПриложения); + АдресХаба = СокрЛП(Строка(КомандаПриложения.ЗначениеОпции("hub"))); + + ПараметрыПриложенияOpm.ПредупредитьОбУстаревшемЗеркале(ИмяСервераПакетов); + + ФайлПакета = ПолучитьФайлПакета(МаскаФайлаПакета); + Цель = ЦельПубликации(АдресХаба, ИмяСервераПакетов); + + Лог.Отладка("Публикация в %1, ресурс %2 (%3)", Цель.Сервер, Цель.Ресурс, Цель.Источник); - Если ПустаяСтрока(ТокенАвторизации) Тогда - НастройкиИзФайла = ПараметрыПриложенияOpm.СобратьНастройкиИзФайлов(); - Если НастройкиИзФайла.Свойство("ТокенПубликацииПакетов") Тогда - ТокенАвторизации = НастройкиИзФайла.ТокенПубликацииПакетов; + Канал = ПолучитьИмяКаналаПубликации(ИмяКанала, Цель.Легаси); + АвторизацияПубликации = ВыбратьАвторизацию(КомандаПриложения, Цель); + + Опубликовать(Цель, АвторизацияПубликации, ФайлПакета, Канал, + ЭтоПринудительнаяПубликация(КомандаПриложения)); + +КонецПроцедуры + +// Куда публикуем: адрес из команды, описанный в opm.cfg сервер либо хаб по умолчанию. +// +// Параметры: +// АдресХаба - Строка - значение опции <--hub>; пустое отправляет за целью в настройки. +// ИмяСервераПакетов - Строка - имя сервера из opm.cfg (устаревшая опция <-m|--mirror>). +// +// Возвращаемое значение: +// Структура - Имя, Сервер, Порт, Ресурс, Пул, БазовыйАдрес, Таймаут, Авторизация, +// Заголовки, Источник, Легаси. +// +Функция ЦельПубликации(Знач АдресХаба, Знач ИмяСервераПакетов = "") Экспорт + + Если ЗначениеЗаполнено(АдресХаба) Тогда + Возврат ЦельИзАдреса(АдресХаба); + КонецЕсли; + + Если ЗначениеЗаполнено(ИмяСервераПакетов) Тогда + Настройка = НайтиНастройкуСервера(ИмяСервераПакетов); + Если Настройка <> Неопределено Тогда + ПроверитьПригодностьКПубликации(Настройка, ИмяСервераПакетов); + Возврат ЦельИзНастройки(Настройка, СтрШаблон("сервер <%1> из opm.cfg", ИмяСервераПакетов)); КонецЕсли; КонецЕсли; - МаскаФайлаПакета = КомандаПриложения.ЗначениеАргумента("FILE"); + Настройка = ПерваяНастройкаДляПубликации(); - Если Не ПустаяСтрока(МаскаФайлаПакетаСтарая) Тогда - Лог.Предупреждение("Использование опции <--file> устарело в следующих версиях будет удалена. Используйте аргумент "); - МаскаФайлаПакета = МаскаФайлаПакетаСтарая; + Если Настройка <> Неопределено Тогда + Цель = ЦельИзНастройки(Настройка, СтрШаблон("сервер <%1> из настроек", Настройка.Имя)); + Лог.Информация("Адрес хаба не указан: публикую в %1 (%2)", Цель.Сервер, Цель.Источник); + Возврат Цель; КонецЕсли; - Ожидаем.Что(МаскаФайлаПакета, СтрШаблон("Не заполнено значение обязательного параметра %1", МаскаФайлаПакета)).Заполнено(); + Лог.Информация("Адрес хаба не указан и в настройках его нет: публикую в %1", + КонстантыOpm.СерверУдаленногоХранилища); - ФайлПакета = ПолучитьФайлПакета(МаскаФайлаПакета); + Возврат НоваяЦель("хаб по умолчанию", Истина); + +КонецФункции + +// Чем авторизуемся в хабе. Доверенная публикация решается набором переменных среды +// и идёт первой: раз конвейер выдал id-token, публикуют именно им, а токен нужен там, +// где доверия вокруг не видно. +// +// Параметры: +// КомандаПриложения - КомандаПриложения - выполняемая команда. +// Цель - Структура - см. ЦельПубликации. +// Конвейер - ТокенКонвейераCI - источник id-token; Неопределено — окружение процесса. +// +// Возвращаемое значение: +// Структура - Заголовок (Строка), Значение (Строка), Доверенная (Булево). +// +Функция ВыбратьАвторизацию(Знач КомандаПриложения, Знач Цель, Знач Конвейер = Неопределено) Экспорт + + Доверие = ПараметрыДоверия(КомандаПриложения); + + ТокенКонвейера = ТокенДоверенногоКонвейера(Доверие.ИмяПеременной, Доверие.Аудитория, + Цель.БазовыйАдрес, Конвейер); + + Если НЕ ПустаяСтрока(ТокенКонвейера) Тогда + Возврат НоваяАвторизация("Authorization", "Bearer " + ТокенКонвейера, Истина); + КонецЕсли; + + ТокенВКоманде = ТокенИзКоманды(КомандаПриложения); + Если НЕ ПустаяСтрока(ТокенВКоманде) Тогда + Возврат АвторизацияТокеном(ТокенВКоманде, Цель); + КонецЕсли; + + Если ЗначениеЗаполнено(Цель.Авторизация) Тогда + Лог.Отладка("Авторизация взята из описания сервера (%1)", Цель.Источник); + Возврат НоваяАвторизация("Authorization", + ОбщегоНазначенияOpm.ПрименитьПеременныеСреды(Цель.Авторизация), Ложь); + КонецЕсли; + + ТокенИзНастроек = ТокенПубликацииИзНастроек(); + Если НЕ ПустаяСтрока(ТокенИзНастроек) Тогда + Лог.Отладка("Токен публикации взят из файла настроек"); + Возврат АвторизацияТокеном(ТокенИзНастроек, Цель); + КонецЕсли; + + ВызватьИсключение СтрШаблон("Для публикации в %1 нужен токен: %2." + + " Доверенная публикация не применяется: %3", + Цель.Имя, ГдеВзятьТокен(Цель), ПричинаОтсутствияДоверия(Доверие.ИмяПеременной, Конвейер)); + +КонецФункции + +// Токен доверенного конвейера либо пустая строка, если доверенной публикации вокруг нет. +// Названная опцией переменная — заявленное намерение: её отсутствие останавливает команду. +// +// Параметры: +// ИмяПеременной - Строка - имя переменной среды с id-token (опция <--oidc-token-var>). +// Аудитория - Строка - клейм aud; пустая берётся равной адресу хаба публикации. +// БазовыйАдресХаба - Строка - адрес хаба публикации. +// Конвейер - ТокенКонвейераCI - источник id-token; Неопределено — окружение процесса. +// +// Возвращаемое значение: +// Строка - значение id-token либо пустая строка. +// +Функция ТокенДоверенногоКонвейера(Знач ИмяПеременной, Знач Аудитория, Знач БазовыйАдресХаба, + Знач Конвейер = Неопределено) Экспорт + + Источник = ?(Конвейер = Неопределено, Новый ТокенКонвейераCI(), Конвейер); + Обнаруженное = Источник.Обнаружить(ИмяПеременной); + + Если НЕ Обнаруженное.Найдено Тогда + Если НЕ ПустаяСтрока(СокрЛП(Строка(ИмяПеременной))) Тогда + ВызватьИсключение СтрШаблон("Доверенная публикация не состоялась: %1", Обнаруженное.Причина); + КонецЕсли; + Лог.Отладка("Доверенная публикация не применяется: %1", Обнаруженное.Причина); + Возврат ""; + КонецЕсли; + + ЗаказаннаяАудитория = ?(ПустаяСтрока(Аудитория), БазовыйАдресХаба, Аудитория); + + Лог.Информация("Публикую доверенным конвейером: %1. Аудитория id-token — %2", + Обнаруженное.Причина, ЗаказаннаяАудитория); + + Предупреждение = ПредупреждениеОбАудитории(ЗаказаннаяАудитория, БазовыйАдресХаба); + Если НЕ ПустаяСтрока(Предупреждение) Тогда + Лог.Предупреждение(Предупреждение); + КонецЕсли; + + Результат = Источник.Получить(ЗаказаннаяАудитория, Обнаруженное.ИмяПеременной); + + Если НЕ Результат.Успех Тогда + ВызватьИсключение СтрШаблон("Доверенная публикация не состоялась: %1", Результат.Сообщение); + КонецЕсли; + + Лог.Отладка("Доверенная публикация: id-token получен (%1)", Результат.Источник); + + Возврат Результат.Токен; + +КонецФункции + +// Требует ли команда публикации поверх уже занятого номера версии: опция <--force>. +// +// Параметры: +// КомандаПриложения - КомандаПриложения - выполняемая команда. +// +// Возвращаемое значение: +// Булево. +// +Функция ЭтоПринудительнаяПубликация(Знач КомандаПриложения) Экспорт + + Возврат ФлагПоднят(КомандаПриложения.ЗначениеОпции("force"), "force"); + +КонецФункции + +// Отправляет файл пакета в хаб и объясняет отказ. +// +// Параметры: +// Цель - Структура - см. ЦельПубликации. +// ЗначениеАвторизации - Структура - см. ВыбратьАвторизацию. +// ФайлПакета - Файл - файл .ospx, который публикуем. +// Канал - Строка - значение заголовка CHANNEL. +// Принудительно - Булево - публиковать поверх уже занятого номера версии. +// +Процедура Опубликовать(Знач Цель, Знач ЗначениеАвторизации, Знач ФайлПакета, Знач Канал, + Знач Принудительно = Ложь) Экспорт + + Заголовки = Новый Соответствие(); + Для Каждого Заголовок Из Цель.Заголовки Цикл + Заголовки.Вставить(Заголовок.Ключ, Заголовок.Значение); + КонецЦикла; + Заголовки.Вставить(ЗначениеАвторизации.Заголовок, ЗначениеАвторизации.Значение); + + Отправитель = Новый ОтправительПакета(Цель.Сервер, Цель.Порт, Заголовки, Цель.Таймаут); + + Результат = Отправитель.Опубликовать(Цель.Ресурс, ФайлПакета.ПолноеИмя, Канал, Принудительно); + + Если НЕ Результат.Успех Тогда + ВызватьИсключение СообщениеОбОтказеПубликации(Результат, ЗначениеАвторизации.Доверенная, Цель.Имя); + КонецЕсли; + + Лог.Информация(Результат.Сообщение); - Канал = ПолучитьИмяКаналаПубликации(ИмяКаналаПубликации); - - ОтправитьПакетВХаб(ТокенАвторизации, ФайлПакета, Канал, ИмяСервераПакетов); - КонецПроцедуры -Функция ПолучитьИмяКаналаПубликации(Знач ИмяКаналаПубликации) - - ГитРепозиторий = Новый ГитРепозиторий(); - ГитРепозиторий.УстановитьРабочийКаталог(ТекущийКаталог()); - - КаналПубликации = СокрЛП(ИмяКаналаПубликации); - - Если КаналПубликации = "auto" Тогда - - Если НЕ ГитРепозиторий.ЭтоРепозиторий() Тогда - ВызватьИсключение "Невозможно автоматически определить канал. " - + "Требуется явное указание в параметре --channel одного из значений: " + ДопустимыеИменаКаналовЗначения(); +// Текст отказа хаба. Про доверенную публикацию говорится отдельно: поддержку доверия +// и запись доверия ведёт хаб, поэтому клиенту видно только код ответа, а человеку нужно +// знать, что не удалась именно она. +// +// Параметры: +// Результат - Структура - ответ ОтправительПакета: Код, Сообщение, Успех. +// Доверенная - Булево - публиковали ли id-token'ом. +// ИмяХаба - Строка - хаб, в который публиковали. +// +// Возвращаемое значение: +// Строка - текст исключения. +// +Функция СообщениеОбОтказеПубликации(Знач Результат, Знач Доверенная, Знач ИмяХаба) Экспорт + + Если НЕ Доверенная Тогда + Возврат Результат.Сообщение; + КонецЕсли; + + Возврат СтрШаблон("Доверенная публикация в %1 не удалась, хаб ответил кодом %2: %3." + + " Хаб может не поддерживать доверенную публикацию либо не иметь записи доверия" + + " для этого пакета — тогда публикуйте токеном", + ИмяХаба, Формат(Результат.Код, "ЧГ=0; ЧН=0"), Результат.Сообщение); + +КонецФункции + +// Текст предупреждения о чужой аудитории. Аудитория задаётся и переменной среды, которая +// переживает смену хаба, а несовпадение хаб покажет только отказом на публикации. +// +// Параметры: +// Аудитория - Строка - аудитория, с которой заказан id-token. +// БазовыйАдресХаба - Строка - адрес хаба публикации. +// +// Возвращаемое значение: +// Строка - текст предупреждения; пустая строка, если предупреждать не о чем. +// +Функция ПредупреждениеОбАудитории(Знач Аудитория, Знач БазовыйАдресХаба) Экспорт + + Если НРег(СокрЛП(Аудитория)) = НРег(СокрЛП(БазовыйАдресХаба)) Тогда + Возврат ""; + КонецЕсли; + + Возврат СтрШаблон("Аудитория id-token <%1> не совпадает с адресом хаба публикации <%2>:" + + " хаб сверяет клейм aud со своим адресом и такую публикацию отклонит", + Аудитория, БазовыйАдресХаба); + +КонецФункции + +/////////////////////////////////////////////////////////////////////////////////////////////////// + +// Значение опции-флага в булево: cli отдаёт поднятый флаг строкой "True", неподнятый — +// булевой Ложью, а <--force=значение> — как написано. Непонятое значение — внятный отказ. +Функция ФлагПоднят(Знач Значение, Знач ИмяОпции) + + Если ТипЗнч(Значение) = Тип("Булево") Тогда + Возврат Значение; + КонецЕсли; + + Написанное = СокрЛП(Строка(Значение)); + + Если ПустаяСтрока(Написанное) Тогда + Возврат Ложь; + КонецЕсли; + + Если Написанное = "1" ИЛИ Написанное = "0" Тогда + Возврат Написанное = "1"; + КонецЕсли; + + Попытка + Возврат Булево(Написанное); + Исключение + ВызватьИсключение СтрШаблон( + "Опция <--%1> — флаг: она либо указана, либо нет. Значение <%2> не понято," + + " из значений принимаются только 1 и 0, да и нет, true и false", + ИмяОпции, Написанное); + КонецПопытки; + +КонецФункции + +Процедура ОписатьОпцииДоверия(Знач КомандаПриложения) + + КомандаПриложения.Опция("oidc-audience", "", "Аудитория id-token (клейм aud). По умолчанию — адрес хаба, + | в который публикуем: хаб сверяет её и отказывает при несовпадении.") + .ВОкружении("OPM_OIDC_AUDIENCE"); + КомандаПриложения.Опция("oidc-token-var", "", "Имя переменной среды с готовым id-token. + | Без неё id-token ищется в переменной id_token, а в GitHub Actions + | заказывается у сервиса токенов конвейера.") + .ВОкружении("OPM_OIDC_TOKEN_VAR"); + +КонецПроцедуры + +// Значение устаревшей опции <-c|--channel>: имя канала либо auto. +// Проверяется сразу, чтобы негодное имя остановило команду до поиска файла и цели. +Функция ИмяКаналаИзКоманды(Знач КомандаПриложения) + + Значение = СокрЛП(Строка(КомандаПриложения.ЗначениеОпции("channel"))); + + Если ПустаяСтрока(Значение) ИЛИ Значение = "auto" Тогда + Возврат "auto"; + КонецЕсли; + + Лог.Предупреждение("Опция <-c|--channel> устарела: канал версии хаб определяет тегами," + + " а адрес пула публикации указывается опцией <--hub>."); + + Если НЕ ЭтоДопустимыйКаналПубликации(Значение) Тогда + ВызватьИсключение "Указано недопустимое имя канала. Допустимые имена:" + Символы.ПС + + ДопустимыеИменаКаналовЗначения(); + КонецЕсли; + + Возврат Значение; + +КонецФункции + +// Маска файла пакета. Адрес хаба аргументом не принимается — только опцией <--hub>. +Функция МаскаФайлаПакетаИзКоманды(Знач КомандаПриложения) + + Маска = СокрЛП(Строка(КомандаПриложения.ЗначениеАргумента("FILE"))); + Устаревшая = СокрЛП(Строка(КомандаПриложения.ЗначениеОпции("file"))); + + Если НЕ ПустаяСтрока(Устаревшая) Тогда + Лог.Предупреждение("Опция <-f|--file> устарела и будет удалена. Используйте аргумент ."); + Если ЗначениеЗаполнено(Маска) Тогда + ВызватьИсключение СтрШаблон("Файл пакета указан дважды: <%1> и <%2>", Маска, Устаревшая); КонецЕсли; - - ИмяВетки = ГитРепозиторий.ПолучитьТекущуюВетку(); - Если ИмяВетки <> "master" Тогда - ВызватьИсключение "Невозможно автоматически определить канал. " - + "Требуется явное указание в параметре --channel одного из значений: " + ДопустимыеИменаКаналовЗначения(); + Маска = Устаревшая; + КонецЕсли; + + Если АдресаХаба.ЭтоАдресХаба(Маска) Тогда + ВызватьИсключение СтрШаблон( + "Аргумент <%1> похож на адрес хаба, а не на файл пакета." + + " Адрес публикации указывается опцией: opm push --hub %1", Маска); + КонецЕсли; + + Возврат ?(ПустаяСтрока(Маска), "*.ospx", Маска); + +КонецФункции + +// Токен, названный в самой команде: стандартный ввод либо опция. +Функция ТокенИзКоманды(Знач КомандаПриложения) + + Если КомандаПриложения.ЗначениеОпции("token-stdin") Тогда + Значение = ""; + ВвестиСтроку(Значение); + Возврат СокрЛП(?(Значение = Неопределено, "", Значение)); + КонецЕсли; + + Возврат СокрЛП(Строка(КомандаПриложения.ЗначениеОпции("token"))); + +КонецФункции + +Функция ТокенПубликацииИзНастроек() + + НастройкиИзФайла = ПараметрыПриложенияOpm.СобратьНастройкиИзФайлов(); + + Если НастройкиИзФайла.Свойство("ТокенПубликацииПакетов") Тогда + Возврат СокрЛП(Строка(НастройкиИзФайла.ТокенПубликацииПакетов)); + КонецЕсли; + + Возврат ""; + +КонецФункции + +Функция ПараметрыДоверия(Знач КомандаПриложения) + + Возврат Новый Структура("Аудитория, ИмяПеременной", + СокрЛП(Строка(КомандаПриложения.ЗначениеОпции("oidc-audience"))), + СокрЛП(Строка(КомандаПриложения.ЗначениеОпции("oidc-token-var")))); + +КонецФункции + +// Объяснение конвейера доходит до человека в отказе «нужен токен», а не только в отладке. +Функция ПричинаОтсутствияДоверия(Знач ИмяПеременной, Знач Конвейер) + + Попытка + Источник = ?(Конвейер = Неопределено, Новый ТокенКонвейераCI(), Конвейер); + Возврат Источник.Обнаружить(ИмяПеременной).Причина; + Исключение + Возврат "конвейер с поддержкой id-token не опрошен"; + КонецПопытки; + +КонецФункции + +Функция НоваяАвторизация(Знач Заголовок, Знач Значение, Знач Доверенная) + + Возврат Новый Структура("Заголовок, Значение, Доверенная", Заголовок, Значение, Доверенная); + +КонецФункции + +// Совет по токену зависит от того, куда публикуем: OpenHub выдаёт токен по opm login, +// а легаси-хаб принимает токен GitHub, никакого входа для него нет. +Функция ГдеВзятьТокен(Знач Цель) + + Если Цель.Легаси Тогда + Возврат "укажите <--token> либо переменную среды GITHUB_OAUTH_TOKEN"; + КонецЕсли; + + Возврат СтрШаблон("выполните opm login %1 либо укажите <--token>", Цель.БазовыйАдрес); + +КонецФункции + +// Легаси-хаб читает токен из заголовка OAUTH-TOKEN и понимает под ним токен GitHub, +// OpenHub — Bearer в Authorization. Различает их маршрут публикации, а не сам токен. +Функция АвторизацияТокеном(Знач Токен, Знач Цель) + + Значение = СокрЛП(Строка(Токен)); + + Если Цель.Легаси Тогда + Возврат НоваяАвторизация("OAUTH-TOKEN", Значение, Ложь); + КонецЕсли; + + Если СтрНачинаетсяС(ВРег(Значение), "BEARER ") Тогда + Возврат НоваяАвторизация("Authorization", Значение, Ложь); + КонецЕсли; + + Возврат НоваяАвторизация("Authorization", "Bearer " + Значение, Ложь); + +КонецФункции + +Функция ЦельИзАдреса(Знач АдресХаба) + + РазобранныйАдрес = АдресаХаба.Разобрать(АдресХаба); + + Если ЗначениеЗаполнено(РазобранныйАдрес.ИмяПакета) Тогда + ВызватьИсключение СтрШаблон( + "Адрес публикации <%1>: ожидался адрес хаба или пула, а не пакета." + + " Уберите имя пакета из адреса", АдресХаба); + КонецЕсли; + + АдресаХаба.ПредупредитьОбОткрытомКанале(РазобранныйАдрес); + + Цель = НоваяЦель("опция <--hub>", Ложь); + Цель.Имя = АдресаХаба.ИмяСервераДляАдреса(РазобранныйАдрес); + Цель.Сервер = СтрШаблон("%1://%2", РазобранныйАдрес.Схема, РазобранныйАдрес.Хост); + Цель.Порт = АдресаХаба.ЭффективныйПорт(РазобранныйАдрес.Схема, РазобранныйАдрес.Порт); + Цель.Ресурс = АдресаХаба.ПутьПубликации(РазобранныйАдрес.Пул); + Цель.Пул = РазобранныйАдрес.Пул; + Цель.БазовыйАдрес = РазобранныйАдрес.БазовыйАдрес; + Цель.Авторизация = НастройкиOpm.АвторизацияДляАдреса(РазобранныйАдрес); + + Возврат Цель; + +КонецФункции + +// Сервер без маршрута публикации (например, файловое зеркало) — повод объясниться, +// а не упасть на обращении к свойству, которого у него не заведено. +Процедура ПроверитьПригодностьКПубликации(Знач НастройкаСервера, Знач ИмяСервераПакетов) + + Если НЕ ПустаяСтрока(РесурсПубликацииНастройки(НастройкаСервера)) Тогда + Возврат; + КонецЕсли; + + ВызватьИсключение СтрШаблон( + "Сервер <%1> из opm.cfg публикацию не принимает: маршрута публикации у него нет." + + " Публикуйте в хаб — его адрес указывается опцией: opm push --hub [схема://]хост[:порт]/пул", + ИмяСервераПакетов); + +КонецПроцедуры + +Функция ЦельИзНастройки(Знач НастройкаСервера, Знач Источник) + + Пул = АдресаХаба.ПулИзПутиПубликации(Строка(НастройкаСервера.РесурсПубликацииПакетов)); + + Цель = НоваяЦель(Источник, ПустаяСтрока(Пул) И ЭтоЛегасиХаб(НастройкаСервера.Сервер)); + Цель.Имя = НастройкаСервера.Имя; + Цель.Сервер = НастройкаСервера.Сервер; + Цель.Порт = ПортИзНастройкиСервера(НастройкаСервера); + Цель.Ресурс = Строка(НастройкаСервера.РесурсПубликацииПакетов); + Цель.Пул = Пул; + Цель.БазовыйАдрес = БазовыйАдресНастройки(НастройкаСервера); + Цель.Таймаут = НастройкаСервера.Таймаут; + Цель.Авторизация = ?(ЗначениеЗаполнено(НастройкаСервера.Авторизация), + Строка(НастройкаСервера.Авторизация), ""); + + Если ЗначениеЗаполнено(НастройкаСервера.ДополнительныеЗаголовки) Тогда + Цель.Заголовки = НастройкаСервера.ДополнительныеЗаголовки; + КонецЕсли; + + Возврат Цель; + +КонецФункции + +// Легаси-протокол — свойство хаба oscript.io: там токен читается из заголовка OAUTH-TOKEN, +// а канал версии обязателен. Прочие хабы говорят по OpenHub, где канал ведут теги пакета. +// +// Параметры: +// АдресСервера - Строка - адрес сервера из настроек. +// +// Возвращаемое значение: +// Булево. +// +Функция ЭтоЛегасиХаб(Знач АдресСервера) Экспорт + + Хост = ХостАдреса(АдресСервера); + + Если ПустаяСтрока(Хост) Тогда + Возврат Ложь; + КонецЕсли; + + Возврат Хост = ХостАдреса(КонстантыOpm.СерверУдаленногоХранилища) + ИЛИ Хост = ХостАдреса(КонстантыOpm.СерверЗапасногоХранилища); + +КонецФункции + +Функция ХостАдреса(Знач Адрес) + + Попытка + Возврат НРег(АдресаХаба.Разобрать(Строка(Адрес)).Хост); + Исключение + Возврат ""; + КонецПопытки; + +КонецФункции + +// Заготовка цели публикации, заполненная значениями хаба по умолчанию. +Функция НоваяЦель(Знач Источник, Знач Легаси) + + Возврат Новый Структура( + "Имя, Сервер, Порт, Ресурс, Пул, БазовыйАдрес, Таймаут, Авторизация, + |Заголовки, Источник, Легаси", + КонстантыOpm.СерверУдаленногоХранилища, КонстантыOpm.СерверУдаленногоХранилища, 0, + КонстантыOpm.РесурсПубликацииПакетов, "", КонстантыOpm.СерверУдаленногоХранилища, + 600, "", Новый Соответствие(), Источник, Легаси); + +КонецФункции + +// Аудитория id-token по умолчанию равна адресу хаба, поэтому у цели из настроек +// порт дописывается ровно тогда, когда он не подразумевается схемой. +Функция БазовыйАдресНастройки(Знач НастройкаСервера) + + Возврат АдресаХаба.АдресСПортом(СокрЛП(Строка(НастройкаСервера.Сервер)), НастройкаСервера.Порт); + +КонецФункции + +Функция НайтиНастройкуСервера(Знач ИмяСервераПакетов) + + Для Каждого НастройкаСервера Из НастройкиOpm.ПолучитьНастройки().СервераПакетов Цикл + Если СтрСравнить(НастройкаСервера.Имя, ИмяСервераПакетов) = 0 Тогда + Возврат НастройкаСервера; КонецЕсли; + КонецЦикла; - Возврат ДопустимыеИменаКаналов.Стабильный; + Возврат Неопределено; - Иначе +КонецФункции + +// Первый описанный сервер, который вообще принимает публикацию: у запасного зеркала +// и у файлового хранилища маршрута публикации нет. +Функция ПерваяНастройкаДляПубликации() - Если ЭтоДопустимыйКаналПубликации(КаналПубликации) Тогда - Возврат КаналПубликации; - Иначе - ТекстСообщения = "Указано недопустимое имя канала. Допустимые имена:" + Символы.ПС; - Для Каждого КлючИЗначение Из ДопустимыеИменаКаналов Цикл - ТекстСообщения = ТекстСообщения + КлючИЗначение.Значение + Символы.ПС; - КонецЦикла; - - ВызватьИсключение ТекстСообщения; + Для Каждого НастройкаСервера Из НастройкиOpm.ПолучитьНастройки().СервераПакетов Цикл + Если НЕ ПустаяСтрока(РесурсПубликацииНастройки(НастройкаСервера)) Тогда + Возврат НастройкаСервера; КонецЕсли; + КонецЦикла; + + Возврат Неопределено; + +КонецФункции + +Функция РесурсПубликацииНастройки(Знач НастройкаСервера) + + Попытка + Значение = НастройкаСервера.РесурсПубликацииПакетов; + Исключение + Возврат ""; + КонецПопытки; + + Возврат ?(ЗначениеЗаполнено(Значение), СокрЛП(Строка(Значение)), ""); + +КонецФункции + +// Поле Порт настройки сервера; порт из самого адреса главнее. +Функция ПортИзНастройкиСервера(Знач НастройкаСервера) + + Если НастройкаСервера.Порт = 0 Тогда + Возврат 0; + КонецЕсли; + + Попытка + Разобранный = АдресаХаба.Разобрать(Строка(НастройкаСервера.Сервер)); + Исключение + Возврат 0; + КонецПопытки; + + Возврат ?(Разобранный.ПортЯвный, 0, НастройкаСервера.Порт); + +КонецФункции + +// Определяет значение заголовка CHANNEL. +// При публикации в пул канал не обязателен: OpenHub хранит его меткой версии, +// а состав каналов ведут теги пакета, поэтому неопределившийся auto даёт пустую метку. +// Легаси-маршрут публикации прежний: auto вне ветки master — ошибка. +Функция ПолучитьИмяКаналаПубликации(Знач ИмяКаналаПубликации, Знач КаналОбязателен) + + Если ИмяКаналаПубликации <> "auto" Тогда + Возврат ИмяКаналаПубликации; + КонецЕсли; + + КаналИзВетки = КаналИзГитРепозитория(); + + Если ЗначениеЗаполнено(КаналИзВетки) Тогда + Возврат КаналИзВетки; + КонецЕсли; + + Если КаналОбязателен Тогда + ВызватьИсключение "Невозможно автоматически определить канал. " + + "Требуется явное указание в параметре --channel одного из значений: " + ДопустимыеИменаКаналовЗначения(); + КонецЕсли; + Возврат ""; + +КонецФункции + +Функция КаналИзГитРепозитория() + + ГитРепозиторий = Новый ГитРепозиторий(); + ГитРепозиторий.УстановитьРабочийКаталог(ТекущийКаталог()); + + Если НЕ ГитРепозиторий.ЭтоРепозиторий() Тогда + Возврат ""; КонецЕсли; + Если ГитРепозиторий.ПолучитьТекущуюВетку() <> "master" Тогда + Возврат ""; + КонецЕсли; + + Возврат ДопустимыеИменаКаналов.Стабильный; + КонецФункции Функция ЭтоДопустимыйКаналПубликации(КаналПубликации) @@ -118,101 +683,47 @@ Прервать; КонецЕсли; КонецЦикла; - + Возврат Результат; КонецФункции +// Принимает и маску в текущем каталоге, и путь к файлу — хоть относительный, хоть полный. Функция ПолучитьФайлПакета(МаскаФайлаПакета) - - НайденныеФайлы = НайтиФайлы(ТекущийКаталог(), МаскаФайлаПакета); - - Если НайденныеФайлы.Количество() = 0 Тогда - ВызватьИсключение "По переданной маске файла пакета не найдено файлов"; + + Указанный = Новый Файл(МаскаФайлаПакета); + Если Указанный.Существует() И Указанный.ЭтоФайл() Тогда + Возврат Указанный; КонецЕсли; - Если НайденныеФайлы.Количество() > 1 Тогда - ВызватьИсключение "По переданной маске файла пакета найдено больше одного файла"; + + Каталог = ТекущийКаталог(); + Маска = МаскаФайлаПакета; + + Если СтрНайти(Маска, ПолучитьРазделительПути()) > 0 ИЛИ СтрНайти(Маска, "/") > 0 Тогда + Каталог = Указанный.Путь; + Маска = Указанный.Имя; КонецЕсли; - - Возврат НайденныеФайлы[0]; - -КонецФункции - -Процедура ОтправитьПакетВХаб(Знач ТокенАвторизации, Знач ФайлПакета, Знач Канал, Знач ИмяСервераПакетов) - - ДвоичныеДанныеФайла = Новый ДвоичныеДанные(ФайлПакета.ПолноеИмя); - ДвоичныеДанныеФайлаВBase64 = Base64Строка(ДвоичныеДанныеФайла); - - ДоступныеСервераПакетов = НастройкиOpm.ПолучитьНастройки().СервераПакетов; - - // Для настроек по умолчанию - Сервер = КонстантыOpm.СерверУдаленногоХранилища; - Ресурс = КонстантыOpm.РесурсПубликацииПакетов; - Авторизация = Неопределено; - ДополнительныеЗаголовки = Неопределено; - Таймаут = 600; - - Для Каждого НастройкаСервера Из ДоступныеСервераПакетов Цикл - - Если СтрСравнить(НастройкаСервера.Имя, ИмяСервераПакетов) = 0 Тогда - Сервер = НастройкаСервера.Сервер; - Ресурс = НастройкаСервера.РесурсПубликацииПакетов; - Авторизация = НастройкаСервера.Авторизация; - ДополнительныеЗаголовки = НастройкаСервера.ДополнительныеЗаголовки; - Таймаут = НастройкаСервера.Таймаут; - Прервать; - КонецЕсли; - - КонецЦикла; - - Лог.Отладка("Сервер = %1", Сервер); - Лог.Отладка("Ресурс = %1", Ресурс); - Заголовки = Новый Соответствие(); - Если ЗначениеЗаполнено(Авторизация) Тогда - Заголовки.Вставить("Authorization", ОбщегоНазначенияOpm.ПрименитьПеременныеСреды(Авторизация)); - Иначе - Заголовки.Вставить("OAUTH-TOKEN", ТокенАвторизации); - КонецЕсли; - Заголовки.Вставить("FILE-NAME", ФайлПакета.Имя); - Заголовки.Вставить("CHANNEL", Канал); - Если ЗначениеЗаполнено(ДополнительныеЗаголовки) Тогда - Для Каждого мЗаголовок Из ДополнительныеЗаголовки Цикл - Заголовки.Вставить(мЗаголовок.Ключ, ОбщегоНазначенияOpm.ПрименитьПеременныеСреды(мЗаголовок.Значение)); - КонецЦикла; - КонецЕсли; - - Соединение = Новый HTTPСоединение(Сервер,,,,, Таймаут); - Запрос = Новый HTTPЗапрос(Ресурс, Заголовки); - Запрос.УстановитьТелоИзДвоичныхДанных(ДвоичныеДанныеФайла); - - Ответ = Соединение.ОтправитьДляОбработки(Запрос); - ТелоОтвета = Ответ.ПолучитьТелоКакСтроку(); - - Если Не КодУспешнойОтправки(Ответ.КодСостояния) Тогда - ВызватьИсключение ТелоОтвета; - КонецЕсли; - - Лог.Информация(ТелоОтвета); - -КонецПроцедуры + НайденныеФайлы = НайтиФайлы(Каталог, Маска); -Функция КодУспешнойОтправки(Знач КодСостояния) + Если НайденныеФайлы.Количество() = 0 Тогда + ВызватьИсключение СтрШаблон("По маске файла пакета <%1> не найдено файлов", МаскаФайлаПакета); + КонецЕсли; + Если НайденныеФайлы.Количество() > 1 Тогда + ВызватьИсключение СтрШаблон("По маске файла пакета <%1> найдено больше одного файла", МаскаФайлаПакета); + КонецЕсли; - Возврат КодСостояния = 200 // OK - Или КодСостояния = 201 // CREATED - Или КодСостояния = 202 // ACCEPTED - ; + Возврат НайденныеФайлы[0]; КонецФункции Функция ДопустимыеИменаКаналовЗначения() - - ДопустимыеИменаКаналовЗначения = Новый Массив; + + Значения = Новый Массив; Для Каждого ИмяКанала Из ДопустимыеИменаКаналов Цикл - ДопустимыеИменаКаналовЗначения.Добавить(ИмяКанала.Значение); + Значения.Добавить(ИмяКанала.Значение); КонецЦикла; - Возврат СтрСоединить(ДопустимыеИменаКаналовЗначения, ", "); + Возврат СтрСоединить(Значения, ", "); КонецФункции 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\260Opm_Update.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\260Opm_Update.os" index 10244f4..85e8d3a 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\260Opm_Update.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\260Opm_Update.os" @@ -22,7 +22,10 @@ ОпцияЗеркала.Перечисление(ДоступноеЗеркало, ДоступноеЗеркало, "Сервер '" + ДоступноеЗеркало + "'"); КонецЦикла; - КомандаПриложения.Аргумент("PACKAGE", "", "Имя пакета в хабе. Чтобы установить конкретную версию, используйте ИмяПакета@ВерсияПакета") + КомандаПриложения.Аргумент("PACKAGE", "", "Имя пакета в хабе. После @ можно указать спецификацию: + | точную версию (пакет@1.0.0), имя тега (пакет@stable) или выражение версии (пакет@^1.4). + | Тег и выражение разрешает хаб; хаб, не принимающий параметр spec, отвечает отказом. + | Можно указать адрес хаба целиком: [схема://]хост[:порт]/[пул/]пакет[@спецификация].") .ТМассивСтрок() .Обязательный(Ложь); @@ -83,7 +86,14 @@ Иначе Для каждого ИмяПакета Из МассивПакетовКОбновлению Цикл - РаботаСПакетами.ОбновитьПакетИзОблака(ИмяПакета, РежимУстановки, , НастройкаУстановки); + + // адрес хаба умеет разбирать только УстановитьПакетПоАдресу + Если АдресаХаба.ЭтоАдресХаба(ИмяПакета) Тогда + РаботаСПакетами.УстановитьПакетПоАдресу(ИмяПакета, РежимУстановки, , НастройкаУстановки); + Иначе + РаботаСПакетами.ОбновитьПакетИзОблака(ИмяПакета, РежимУстановки, , НастройкаУстановки); + КонецЕсли; + КонецЦикла; КонецЕсли; diff --git "a/src/cmd/\320\234\320\276\320\264\321\203\320\273\320\270/\320\237\320\260\321\200\320\260\320\274\320\265\321\202\321\200\321\213\320\237\321\200\320\270\320\273\320\276\320\266\320\265\320\275\320\270\321\217Opm.os" "b/src/cmd/\320\234\320\276\320\264\321\203\320\273\320\270/\320\237\320\260\321\200\320\260\320\274\320\265\321\202\321\200\321\213\320\237\321\200\320\270\320\273\320\276\320\266\320\265\320\275\320\270\321\217Opm.os" index 034828c..87a295b 100644 --- "a/src/cmd/\320\234\320\276\320\264\321\203\320\273\320\270/\320\237\320\260\321\200\320\260\320\274\320\265\321\202\321\200\321\213\320\237\321\200\320\270\320\273\320\276\320\266\320\265\320\275\320\270\321\217Opm.os" +++ "b/src/cmd/\320\234\320\276\320\264\321\203\320\273\320\270/\320\237\320\260\321\200\320\260\320\274\320\265\321\202\321\200\321\213\320\237\321\200\320\270\320\273\320\276\320\266\320\265\320\275\320\270\321\217Opm.os" @@ -15,6 +15,54 @@ Возврат "oscript.app.opm"; КонецФункции +// Добавляет команде опции управления сверкой контрольной суммы скачанного файла. +// +// Параметры: +// КомандаПриложения - КомандаПриложения - описываемая команда. +// +Процедура ОписатьОпцииПроверкиСуммы(Знач КомандаПриложения) Экспорт + + КомандаПриложения.Опция("verify-checksum", Ложь, + "Строгая сверка контрольной суммы: хаб обязан прислать X-Checksum-Sha256.") + .ВОкружении("OPM_VERIFY_CHECKSUM"); + КомандаПриложения.Опция("no-verify-checksum", Ложь, + "Не сверять контрольную сумму скачанного файла."); + +КонецПроцедуры + +// Применяет опции сверки контрольной суммы к настройкам прогона. +// По умолчанию действует мягкий режим: заголовка нет — ставим как раньше. +// +// Параметры: +// КомандаПриложения - КомандаПриложения - выполняемая команда. +// +Процедура ПрименитьОпцииПроверкиСуммы(Знач КомандаПриложения) Экспорт + + Если КомандаПриложения.ЗначениеОпции("no-verify-checksum") Тогда + НастройкиOpm.УстановитьПроверкуКонтрольнойСуммы(КонтрольныеСуммы.РежимВыключено()); + ИначеЕсли КомандаПриложения.ЗначениеОпции("verify-checksum") Тогда + НастройкиOpm.УстановитьПроверкуКонтрольнойСуммы(КонтрольныеСуммы.РежимСтрогий()); + КонецЕсли; + +КонецПроцедуры + +// Предупреждает об устаревшей опции выбора зеркала по имени из конфигурации. +// +// Параметры: +// ИмяСервера - Строка - значение опции; пустое ничего не печатает. +// +Процедура ПредупредитьОбУстаревшемЗеркале(Знач ИмяСервера) Экспорт + + Если ПустаяСтрока(ИмяСервера) Тогда + Возврат; + КонецЕсли; + + Лог.Предупреждение( + "Опция <-m|--mirror> устарела и будет удалена. Укажите адрес хаба прямо в команде: " + + "хост[:порт]/пул/пакет[@спецификация]."); + +КонецПроцедуры + Процедура НастроитьOpm() Экспорт НастройкиOpmИзФайлов = СобратьНастройкиИзФайлов(); @@ -289,28 +337,7 @@ // Текущий каталог запуска, домашний каталог пользователя, системная настройка, каталог opm (для совместимости). Функция СписокФайловНастроек() - ИмяФайл = КонстантыOpm.ИмяФайлаНастроек; - МассивФайлов = Новый Массив; - //Текущий каталог - МассивФайлов.Добавить(ОбъединитьПути(ТекущийКаталог(), ИмяФайл)); - //Настройки в профиле пользователя. - ПутьКНастройкам = ""; - СИ = Новый СистемнаяИнформация(); - Если Найти(Нрег(СИ.ВерсияОС), Нрег("Windows")) > 0 Тогда - ПутьКНастройкам = ОбъединитьПути(ПолучитьПеременнуюСреды(Врег("USERPROFILE")), ИмяФайл); - ПутьКНастройкамСистемный = ОбъединитьПути(ПолучитьПеременнуюСреды(Врег("ALLUSERSPROFILE")), ИмяФайл); - Иначе - ПутьКНастройкам = ОбъединитьПути(ПолучитьПеременнуюСреды(Врег("HOME")), "."+ИмяФайл); - ПутьКНастройкамСистемный = ОбъединитьПути("/etc", ИмяФайл); - КонецЕсли; - - МассивФайлов.Добавить(ПутьКНастройкам); - МассивФайлов.Добавить(ПутьКНастройкамСистемный); - - //Совместимость со старым поведением - МассивФайлов.Добавить(ОбъединитьПути(СтартовыйСценарий().Каталог, ИмяФайл)); - - Возврат МассивФайлов; + Возврат ФайлыНастроекOpm.СписокФайловНастроек(); КонецФункции diff --git "a/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\222\321\205\320\276\320\264\320\222\320\245\320\260\320\261.os" "b/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\222\321\205\320\276\320\264\320\222\320\245\320\260\320\261.os" new file mode 100644 index 0000000..c7ba6c4 --- /dev/null +++ "b/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\222\321\205\320\276\320\264\320\222\320\245\320\260\320\261.os" @@ -0,0 +1,294 @@ +// Вход в хаб пакетов: проверка токена и получение токена через браузер +// (device authorization grant, RFC 8628). + +#Использовать logos + +Перем Лог; + +Перем РазобранныйАдрес; +Перем Таймаут; + +Процедура ПриСозданииОбъекта(Знач ВходящийРазобранныйАдрес, Знач ВходящийТаймаут = 60) + + РазобранныйАдрес = ВходящийРазобранныйАдрес; + Таймаут = ВходящийТаймаут; + +КонецПроцедуры + +// Проверяет токен и возвращает сведения об учётной записи. +// +// Параметры: +// Токен - Строка - значение персонального токена доступа. +// +// Возвращаемое значение: +// Структура - Успех (Булево), Логин (Строка), Код (Число), Сообщение (Строка). +// +Функция ПроверитьТокен(Знач Токен) Экспорт + + Заголовки = Новый Соответствие(); + Заголовки.Вставить("Authorization", "Bearer " + СокрЛП(Строка(Токен))); + + Ответ = Запрос("GET", "/api/v1/whoami", Заголовки, ""); + + Если Ответ.Код <> 200 Тогда + Возврат Новый Структура("Успех, Логин, Код, Сообщение", Ложь, "", Ответ.Код, + ТекстОшибкиAPI(Ответ)); + КонецЕсли; + + Разобранный = ОбщегоНазначенияOpm.РазобратьJSONОбъект(Ответ.Тело); + Логин = ""; + Если Разобранный <> Неопределено Тогда + Пользователь = Разобранный.Получить("user"); + Если ТипЗнч(Пользователь) = Тип("Соответствие") Тогда + Логин = Строка(Пользователь.Получить("login")); + КонецЕсли; + КонецЕсли; + + Возврат Новый Структура("Успех, Логин, Код, Сообщение", Истина, Логин, 200, ""); + +КонецФункции + +// Инициирует вход через браузер: просит у хаба код устройства и код пользователя. +// +// Параметры: +// ИмяКлиента - Строка - как назвать выданный токен в кабинете. +// Права - Строка - запрашиваемые скоупы через запятую. +// +// Возвращаемое значение: +// Структура - Успех (Булево), КодУстройства (Строка), КодПользователя (Строка), +// АдресПодтверждения (Строка), АдресПодтвержденияСКодом (Строка), +// СрокСекунд (Число), ИнтервалСекунд (Число), Код (Число), Сообщение (Строка). +// +Функция НачатьВходЧерезБраузер(Знач ИмяКлиента, Знач Права = "read,write") Экспорт + + Тело = Новый Структура(); + Тело.Вставить("client_name", ИмяКлиента); + Тело.Вставить("scopes", Права); + + Ответ = Запрос("POST", "/api/v1/cli/auth/device", ЗаголовкиJSON(), ЗаписатьJSONСтрокой(Тело)); + + Если Ответ.Код <> 200 И Ответ.Код <> 201 Тогда + Возврат Отказ(Ответ); + КонецЕсли; + + Данные = ОбщегоНазначенияOpm.РазобратьJSONОбъект(Ответ.Тело); + Если Данные = Неопределено Тогда + Возврат Новый Структура("Успех, Код, Сообщение", Ложь, Ответ.Код, "Хаб вернул неразбираемый ответ"); + КонецЕсли; + + Возврат Новый Структура( + "Успех, Код, Сообщение, КодУстройства, КодПользователя, + |АдресПодтверждения, АдресПодтвержденияСКодом, СрокСекунд, ИнтервалСекунд", + Истина, Ответ.Код, "", + Строка(Данные.Получить("device_code")), + Строка(Данные.Получить("user_code")), + АбсолютныйАдрес(Данные.Получить("verification_uri")), + АбсолютныйАдрес(Данные.Получить("verification_uri_complete")), + ЧислоИлиДефолт(Данные.Получить("expires_in"), 600), + ЧислоИлиДефолт(Данные.Получить("interval"), 5)); + +КонецФункции + +// Опрашивает хаб, пока человек не подтвердит вход в браузере. +// Значение токена уходит только в тело ответа: в адресной строке его нет никогда. +// +// Параметры: +// КодУстройства - Строка - секрет, выданный при инициации. +// СрокСекунд - Число - срок жизни заявки. +// ИнтервалСекунд - Число - рекомендованный хабом интервал опроса. +// +// Возвращаемое значение: +// Структура - Успех (Булево), Токен (Строка), Права (Строка), Ошибка (Строка), Сообщение (Строка). +// +Функция ДождатьсяПодтверждения(Знач КодУстройства, Знач СрокСекунд, Знач ИнтервалСекунд) Экспорт + + Пауза = Макс(1, ИнтервалСекунд); + Дедлайн = ТекущаяУниверсальнаяДата() + Макс(30, СрокСекунд); + + Пока ТекущаяУниверсальнаяДата() <= Дедлайн Цикл + + Тело = Новый Структура("device_code", КодУстройства); + Ответ = Запрос("POST", "/api/v1/cli/auth/device/token", ЗаголовкиJSON(), ЗаписатьJSONСтрокой(Тело)); + + Если Ответ.Код = 200 ИЛИ Ответ.Код = 201 Тогда + Данные = ОбщегоНазначенияOpm.РазобратьJSONОбъект(Ответ.Тело); + Если Данные = Неопределено Тогда + Возврат НеудачаОжидания("invalid_response", "Хаб вернул неразбираемый ответ"); + КонецЕсли; + Возврат Новый Структура("Успех, Токен, Права, Ошибка, Сообщение", + Истина, Строка(Данные.Получить("access_token")), + Строка(Данные.Получить("scope")), "", ""); + КонецЕсли; + + КодОшибки = КодОшибкиОжидания(Ответ); + + Если КодОшибки = "authorization_pending" Тогда + Приостановить(Пауза * 1000); + Продолжить; + КонецЕсли; + + Если КодОшибки = "slow_down" Тогда + Пауза = Пауза + 5; + Приостановить(Пауза * 1000); + Продолжить; + КонецЕсли; + + Если КодОшибки = "access_denied" Тогда + Возврат НеудачаОжидания(КодОшибки, "Вход отклонён в браузере"); + КонецЕсли; + + Если КодОшибки = "expired_token" Тогда + Возврат НеудачаОжидания(КодОшибки, "Срок заявки на вход истёк — повторите opm login"); + КонецЕсли; + + Возврат НеудачаОжидания(КодОшибки, ТекстОшибкиAPI(Ответ)); + + КонецЦикла; + + Возврат НеудачаОжидания("expired_token", "Подтверждение не получено за отведённое время"); + +КонецФункции + +// Адрес, по которому человек подтверждает вход: с подставленным кодом, если хаб его дал. +// +// Параметры: +// Заявка - Структура - результат НачатьВходЧерезБраузер. +// +// Возвращаемое значение: +// Строка. +// +Функция АдресПодтвержденияДляЧеловека(Знач Заявка) Экспорт + + Возврат ?(ЗначениеЗаполнено(Заявка.АдресПодтвержденияСКодом), + Заявка.АдресПодтвержденияСКодом, Заявка.АдресПодтверждения); + +КонецФункции + +/////////////////////////////////////////////////////////////////// + +Функция Запрос(Знач Метод, Знач Ресурс, Знач Заголовки, Знач Тело) + + Соединение = Новый HTTPСоединение( + СтрШаблон("%1://%2", РазобранныйАдрес.Схема, РазобранныйАдрес.Хост), + АдресаХаба.ЭффективныйПорт(РазобранныйАдрес.Схема, РазобранныйАдрес.Порт), , , , Таймаут); + + HTTPЗапрос = Новый HTTPЗапрос(Ресурс, Заголовки); + Если ЗначениеЗаполнено(Тело) Тогда + HTTPЗапрос.УстановитьТелоИзСтроки(Тело); + КонецЕсли; + + Попытка + Если ВРег(Метод) = "GET" Тогда + Ответ = Соединение.Получить(HTTPЗапрос); + Иначе + Ответ = Соединение.ОтправитьДляОбработки(HTTPЗапрос); + КонецЕсли; + Исключение + // код 0 означает «до хаба не дозвонились»; служебная обёртка платформы — в отладку + ПодробноеОписание = ОписаниеОшибки(); + Лог.Отладка("Запрос %1%2 не удался: %3", РазобранныйАдрес.БазовыйАдрес, Ресурс, ПодробноеОписание); + Возврат Новый Структура("Код, Тело", 0, + ОшибкиСети.НеУдалосьПодключиться(РазобранныйАдрес.БазовыйАдрес, ПодробноеОписание)); + КонецПопытки; + + Возврат Новый Структура("Код, Тело", Ответ.КодСостояния, Ответ.ПолучитьТелоКакСтроку()); + +КонецФункции + +Функция ЗаголовкиJSON() + + Заголовки = Новый Соответствие(); + Заголовки.Вставить("Content-Type", "application/json; charset=utf-8"); + Заголовки.Вставить("Accept", "application/json"); + + Возврат Заголовки; + +КонецФункции + +Функция ЗаписатьJSONСтрокой(Знач Данные) + + Запись = Новый ЗаписьJSON(); + Запись.УстановитьСтроку(); + ЗаписатьJSON(Запись, Данные); + + Возврат Запись.Закрыть(); + +КонецФункции + +Функция КодОшибкиОжидания(Знач Ответ) + + Данные = ОбщегоНазначенияOpm.РазобратьJSONОбъект(Ответ.Тело); + Если Данные = Неопределено Тогда + Возврат ""; + КонецЕсли; + + Значение = Данные.Получить("error"); + + Если ТипЗнч(Значение) = Тип("Соответствие") Тогда + Возврат НРег(СокрЛП(Строка(Значение.Получить("code")))); + КонецЕсли; + + Возврат НРег(СокрЛП(Строка(Значение))); + +КонецФункции + +Функция ТекстОшибкиAPI(Знач Ответ) + + Данные = ОбщегоНазначенияOpm.РазобратьJSONОбъект(Ответ.Тело); + + Если Данные <> Неопределено Тогда + Ошибка = Данные.Получить("error"); + // две формы: {"error":{"code","message"}} у JSON-API хаба + // и плоские {"error","error_description"} у потока RFC 8628 + Если ТипЗнч(Ошибка) = Тип("Соответствие") Тогда + Возврат Строка(Ошибка.Получить("message")); + КонецЕсли; + Если Данные.Получить("error_description") <> Неопределено Тогда + Возврат Строка(Данные.Получить("error_description")); + КонецЕсли; + Если Данные.Получить("Сообщение") <> Неопределено Тогда + Возврат Строка(Данные.Получить("Сообщение")); + КонецЕсли; + КонецЕсли; + + Возврат ?(Ответ.Код = 0, Ответ.Тело, СтрШаблон("Хаб ответил кодом %1", + Формат(Ответ.Код, "ЧГ=0; ЧН=0"))); + +КонецФункции + +Функция АбсолютныйАдрес(Знач Значение) + + Адрес = СокрЛП(Строка(Значение)); + + Если ПустаяСтрока(Адрес) Тогда + Возврат ""; + КонецЕсли; + + Если СтрНачинаетсяС(НРег(Адрес), "http://") ИЛИ СтрНачинаетсяС(НРег(Адрес), "https://") Тогда + Возврат Адрес; + КонецЕсли; + + Возврат РазобранныйАдрес.БазовыйАдрес + ?(СтрНачинаетсяС(Адрес, "/"), "", "/") + Адрес; + +КонецФункции + +Функция ЧислоИлиДефолт(Знач Значение, Знач Дефолт) + + Попытка + Возврат ?(Значение = Неопределено, Дефолт, Число(Значение)); + Исключение + Возврат Дефолт; + КонецПопытки; + +КонецФункции + +Функция Отказ(Знач Ответ) + Возврат Новый Структура("Успех, Код, Сообщение", Ложь, Ответ.Код, ТекстОшибкиAPI(Ответ)); +КонецФункции + +Функция НеудачаОжидания(Знач КодОшибки, Знач Сообщение) + Возврат Новый Структура("Успех, Токен, Права, Ошибка, Сообщение", + Ложь, "", "", КодОшибки, Сообщение); +КонецФункции + +Лог = Логирование.ПолучитьЛог("oscript.app.opm.login"); diff --git "a/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\227\320\260\320\277\321\200\320\276\321\201\321\207\320\270\320\272HTTP.os" "b/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\227\320\260\320\277\321\200\320\276\321\201\321\207\320\270\320\272HTTP.os" new file mode 100644 index 0000000..93f0856 --- /dev/null +++ "b/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\227\320\260\320\277\321\200\320\276\321\201\321\207\320\270\320\272HTTP.os" @@ -0,0 +1,76 @@ +// GET по произвольному адресу. Отдельный объект, чтобы обращение к сервису токенов +// конвейера проверялось тестами без сети. + +#Использовать logos + +Перем Лог; + +Перем Таймаут; + +Процедура ПриСозданииОбъекта(Знач ВходящийТаймаут = 30) + + Таймаут = ВходящийТаймаут; + +КонецПроцедуры + +// Выполняет GET-запрос. +// +// Параметры: +// Адрес - Строка - полный адрес запроса со схемой, при необходимости с параметрами. +// Заголовки - Соответствие - заголовки запроса. +// +// Возвращаемое значение: +// Структура - Код (Число, 0 — до сервера не дозвонились), Тело (Строка; при коде 0 — +// человеческая причина отказа связи). +// +Функция Получить(Знач Адрес, Знач Заголовки) Экспорт + + Разобранный = Разобрать(Адрес); + + Попытка + Соединение = Новый HTTPСоединение(Разобранный.Основа, Разобранный.Порт, , , , Таймаут); + Ответ = Соединение.Получить(Новый HTTPЗапрос(Разобранный.Ресурс, Заголовки)); + Исключение + ПодробноеОписание = ОписаниеОшибки(); + Лог.Отладка("Запрос %1 не удался: %2", Разобранный.Основа, ПодробноеОписание); + Возврат Новый Структура("Код, Тело", 0, + ОшибкиСети.НеУдалосьПодключиться(Разобранный.Основа, ПодробноеОписание)); + КонецПопытки; + + Возврат Новый Структура("Код, Тело", Ответ.КодСостояния, Ответ.ПолучитьТелоКакСтроку()); + +КонецФункции + +/////////////////////////////////////////////////////////////////// + +// Делит адрес на основу (схема с хостом), порт и ресурс с параметрами. +Функция Разобрать(Знач Адрес) + + Значение = СокрЛП(Строка(Адрес)); + + Позиция = СтрНайти(Значение, "://"); + Если Позиция = 0 Тогда + ВызватьИсключение СтрШаблон("Адрес <%1> без схемы: ожидался http:// или https://", Адрес); + КонецЕсли; + + Схема = НРег(Лев(Значение, Позиция - 1)); + Остаток = Сред(Значение, Позиция + 3); + + КонецАвторитета = СтрНайти(Остаток, "/"); + Авторитет = ?(КонецАвторитета = 0, Остаток, Лев(Остаток, КонецАвторитета - 1)); + Ресурс = ?(КонецАвторитета = 0, "/", Сред(Остаток, КонецАвторитета)); + + Хост = Авторитет; + Порт = ?(Схема = "https", 443, 80); + + ПозицияПорта = СтрНайти(Авторитет, ":", НаправлениеПоиска.СКонца); + Если ПозицияПорта > 0 И СтрНайти(Авторитет, "]") < ПозицияПорта Тогда + Хост = Лев(Авторитет, ПозицияПорта - 1); + Порт = Число(Сред(Авторитет, ПозицияПорта + 1)); + КонецЕсли; + + Возврат Новый Структура("Основа, Порт, Ресурс", СтрШаблон("%1://%2", Схема, Хост), Порт, Ресурс); + +КонецФункции + +Лог = Логирование.ПолучитьЛог("oscript.app.opm"); diff --git "a/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\227\320\260\320\277\321\203\321\201\320\272\320\260\321\202\320\265\320\273\321\214\320\237\321\200\320\276\320\263\321\200\320\260\320\274\320\274.os" "b/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\227\320\260\320\277\321\203\321\201\320\272\320\260\321\202\320\265\320\273\321\214\320\237\321\200\320\276\320\263\321\200\320\260\320\274\320\274.os" new file mode 100644 index 0000000..8ab504a --- /dev/null +++ "b/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\227\320\260\320\277\321\203\321\201\320\272\320\260\321\202\320\265\320\273\321\214\320\237\321\200\320\276\320\263\321\200\320\260\320\274\320\274.os" @@ -0,0 +1,51 @@ +// Запуск внешней программы: исполняемое отдельно, параметры отдельно. Выделен +// отдельным объектом, чтобы выбор «чем открыть» в ОткрывателеБраузера проверялся +// тестами без запуска процессов. +// +// Командный процессор системы из запуска исключён намеренно: 1commands по умолчанию +// заворачивает строку запуска в `sh -c '...'` либо `cmd /c "..."`, и тогда всё, что +// попало в параметры, читает ещё и оболочка. + +#Использовать 1commands + +// Исполняет команду и дожидается завершения. +// +// Параметры: +// ИмяКоманды - Строка - исполняемое. +// Параметры - Массив из Строка, Неопределено - параметры команды. +// +// Возвращаемое значение: +// Структура - КодВозврата (Число, -1 — запустить не удалось), Ошибка (Строка), +// Вывод (Строка - то, что программа написала в потоки). +// +Функция Запустить(Знач ИмяКоманды, Знач Параметры = Неопределено) Экспорт + + Попытка + Команда = Новый Команда(); + Команда.УстановитьКоманду(ИмяКоманды); + Команда.УстановитьИсполнениеЧерезКомандыСистемы(Ложь); + Если Параметры <> Неопределено Тогда + Команда.ДобавитьПараметры(Параметры); + КонецЕсли; + Команда.ПоказыватьВыводНемедленно(Ложь); + КодВозврата = Команда.Исполнить(); + Возврат Новый Структура("КодВозврата, Ошибка, Вывод", КодВозврата, "", ВыводКоманды(Команда)); + Исключение + Возврат Новый Структура("КодВозврата, Ошибка, Вывод", -1, ОписаниеОшибки(), ""); + КонецПопытки; + +КонецФункции + +/////////////////////////////////////////////////////////////////// + +// Вывод программы нужен только для объяснения сбоя, поэтому его отсутствие +// не должно ломать сам запуск. +Функция ВыводКоманды(Знач Команда) + + Попытка + Возврат СокрЛП(Строка(Команда.ПолучитьВывод())); + Исключение + Возврат ""; + КонецПопытки; + +КонецФункции diff --git "a/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\234\320\265\320\275\320\265\320\264\320\266\320\265\321\200\320\237\320\276\320\273\321\203\321\207\320\265\320\275\320\270\321\217\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" "b/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\234\320\265\320\275\320\265\320\264\320\266\320\265\321\200\320\237\320\276\320\273\321\203\321\207\320\265\320\275\320\270\321\217\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" index 7355e45..978d663 100644 --- "a/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\234\320\265\320\275\320\265\320\264\320\266\320\265\321\200\320\237\320\276\320\273\321\203\321\207\320\265\320\275\320\270\321\217\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" +++ "b/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\234\320\265\320\275\320\265\320\264\320\266\320\265\321\200\320\237\320\276\320\273\321\203\321\207\320\265\320\275\320\270\321\217\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" @@ -10,25 +10,36 @@ КонецПроцедуры -Функция ПолучитьПакет(Знач ИмяПакета, Знач ВерсияПакета, ПутьКФайлуПакета = "", ИмяСервера = "") Экспорт +// Скачивает файл пакета из хаба. +// +// Параметры: +// ИмяПакета - Строка - имя пакета в хабе. +// Спецификация - Строка, Неопределено - то, что пользователь написал после @: точный номер +// версии, имя тега либо выражение версии; Неопределено — версию выбирает хаб. +// ПутьКФайлуПакета - Строка - куда положить скачанный файл; пустое даёт временный файл. +// ИмяСервера - Строка - имя сервера пакетов; пустое означает перебор всех известных. +// +// Возвращаемое значение: +// Строка - путь к скачанному файлу пакета. +// +Функция ПолучитьПакет(Знач ИмяПакета, Знач Спецификация, ПутьКФайлуПакета = "", ИмяСервера = "") Экспорт ОбновитьИндексыКешейПакетов(); Если Не ПакетДоступен(ИмяПакета) Тогда - - ТекстИсключения = СтрШаблон("Ошибка установки пакета %1: Пакет не найден", ИмяПакета); - ВызватьИсключение ТекстИсключения; - + ВызватьИсключение ОтказУстановки(ИмяПакета, "Пакет не найден"); КонецЕсли; ИмяПакета = ОпределитьИмяАрхива(ИмяПакета); - Если ВерсияПакета <> Неопределено Тогда - ФайлПакета = ИмяПакета + "-" + ВерсияПакета + ".ospx"; - Иначе - ФайлПакета = ИмяПакета + ".ospx"; + // пустая спецификация не схлопывается в Неопределено: «pkg@» — отказ, а не выбор хаба + Если Спецификация <> Неопределено Тогда + Спецификация = СокрЛП(Строка(Спецификация)); КонецЕсли; + ПроверитьВыразимостьСпецификации(ИмяПакета, Спецификация); + ФайлПакета = ИмяФайлаПакета(ИмяПакета, Спецификация); + ОбщегоНазначенияOpm.СообщениеПользователю(Лог, "Скачиваю файл: " + ФайлПакета); Если ПустаяСтрока(ПутьКФайлуПакета) Тогда @@ -39,32 +50,172 @@ ПереченьСерверов = ИндексКешаПакетов[ИмяПакета]; - Ответ = ЗапроситьПакет(ПереченьСерверов, ИмяСервера, ИмяРесурса); + Полученное = ЗапроситьПакет(ПереченьСерверов, ИмяСервера, ИмяРесурса, Спецификация); + Ответ = Полученное.Ответ; - Если Не Ответ = Неопределено Тогда - Лог.Отладка("Файл получен"); - Ответ.ПолучитьТелоКакДвоичныеДанные().Записать(ПутьКФайлуПакета); - Ответ.Закрыть(); - Лог.Отладка("Соединение закрыто"); - Иначе - ТекстИсключения = СтрШаблон("Ошибка установки пакета %1: Нет соединения", ИмяПакета); - ВызватьИсключение ТекстИсключения; + Если Ответ = Неопределено Тогда + Если ЗначениеЗаполнено(Полученное.ОтказТранспорта) Тогда + ВызватьИсключение Полученное.ОтказТранспорта; + КонецЕсли; + ВызватьИсключение ОтказУстановки(ИмяПакета, + ПричинаНеудачи(ФайлПакета, Полученное.КодСостояния, Спецификация)); КонецЕсли; + Лог.Отладка("Файл получен"); + ЗаголовкиОтвета = ЗаголовкиОтвета(Ответ); + Ответ.ПолучитьТелоКакДвоичныеДанные().Записать(ПутьКФайлуПакета); + Ответ.Закрыть(); + Лог.Отладка("Соединение закрыто"); + + ПроверитьЗагруженныйФайл(ПутьКФайлуПакета, ЗаголовкиОтвета, ФайлПакета); + Возврат ПутьКФайлуПакета; КонецФункции -Функция ЗапроситьПакет(Знач ПереченьСерверов, Знач ИмяСервера, Знач ИмяРесурса) +// Файл, не прошедший сверку, на диске не оставляем: путь загрузки задаёт вызывающий, +// и непроверенный артефакт по этому пути — ровно то, от чего сверка защищает. +Процедура ПроверитьЗагруженныйФайл(Знач ПутьКФайлуПакета, Знач ЗаголовкиОтвета, Знач ФайлПакета) + + Попытка + КонтрольныеСуммы.ПроверитьЗагрузку(ПутьКФайлуПакета, ЗаголовкиОтвета, ФайлПакета, + НастройкиOpm.ПолучитьНастройки().ПроверкаКонтрольнойСуммы); + Исключение + УдалитьНепроверенныйФайл(ПутьКФайлуПакета); + ВызватьИсключение; + КонецПопытки; + +КонецПроцедуры + +// Не удалить файл — беда меньшая, чем потерять причину отказа сверки, поэтому +// исключение проверки наружу уходит в любом случае. +Процедура УдалитьНепроверенныйФайл(Знач ПутьКФайлу) + + Попытка + УдалитьФайлы(ПутьКФайлу); + Исключение + Лог.Предупреждение("Не удалось удалить непроверенный файл %1: %2", + ПутьКФайлу, ОписаниеОшибки()); + КонецПопытки; + +КонецПроцедуры + +// Имя артефакта остаётся легаси-формы и для тега: старый хаб ответит на него 404 +// вместо молчаливой подмены последней версией. +Функция ИмяФайлаПакета(Знач ИмяПакета, Знач Спецификация) + + Если Спецификация = Неопределено Тогда + Возврат ИмяПакета + ".ospx"; + КонецЕсли; + + Возврат СтрШаблон("%1-%2.ospx", ИмяПакета, Спецификация); + +КонецФункции + +// Косая черта в спецификации разрезала бы путь запроса — отказ до запроса. +Процедура ПроверитьВыразимостьСпецификации(Знач ИмяПакета, Знач Спецификация) + + Если Спецификация = Неопределено ИЛИ СтрНайти(Спецификация, "/") = 0 Тогда + Возврат; + КонецЕсли; + + ВызватьИсключение ОтказУстановки(ИмяПакета, СтрШаблон( + "спецификация <%1> содержит косую черту." + + " Имя артефакта такую спецификацию не выражает — переименуйте тег либо укажите версию", + Спецификация)); + +КонецПроцедуры + +// Фасад отказа: все неудачи установки пакета выглядят для пользователя одинаково. +Функция ОтказУстановки(Знач ИмяПакета, Знач Причина) + + Возврат СтрШаблон("Ошибка установки пакета %1: %2", ИмяПакета, Причина); + +КонецФункции + +// Различает «хаб ответил, но файла нет» и «до хаба не достучались»: старое сообщение +// про отсутствие соединения на 404 сбивало с толку. +Функция ПричинаНеудачи(Знач ФайлПакета, Знач КодСостояния, Знач Спецификация) + + Если КодСостояния = 404 Тогда + Возврат ПричинаОтсутствияАртефакта(ФайлПакета, Спецификация); + ИначеЕсли КодСостояния = 401 ИЛИ КодСостояния = 403 Тогда + Возврат СтрШаблон("хаб отказал в доступе к %1 (код %2). Нужен вход: opm login <адрес>", + ФайлПакета, Формат(КодСостояния, "ЧГ=0; ЧН=0")); + ИначеЕсли КодСостояния <> 0 Тогда + Возврат СтрШаблон("хаб ответил кодом %1 на запрос %2", + Формат(КодСостояния, "ЧГ=0; ЧН=0"), ФайлПакета); + КонецЕсли; + + Возврат "Нет соединения"; + +КонецФункции + +// Хаб сказал «нет такого артефакта». Теги и диапазоны разрешает только хаб, +// принимающий параметр spec, — на нём и стоит объяснение отказа. +Функция ПричинаОтсутствияАртефакта(Знач ФайлПакета, Знач Спецификация) + + Если ЗначениеЗаполнено(Спецификация) И НЕ РаботаСВерсиями.ЭтоТочныйНомерВерсии(Спецификация) Тогда + Возврат СтрШаблон("хаб не умеет разрешать спецификацию <%1> и ответил 404." + + " По тегу и по выражению версии ставит только хаб, принимающий параметр spec;" + + " хабу без него нужен точный номер версии", Спецификация); + КонецЕсли; + + Возврат СтрШаблон("файл %1 в хабе не найден", ФайлПакета); + +КонецФункции + +Функция ЗаголовкиОтвета(Знач Ответ) + + Попытка + Возврат Ответ.Заголовки; + Исключение + Возврат Неопределено; + КонецПопытки; + +КонецФункции + +// Текст отказа, когда пакет искали ровно на одном сервере и до него не дозвонились. +// Файловый сервер пакетов транспортной ошибки не имеет — у него пустая строка. +Функция ОтказЕдинственногоСервера(Знач Сервер) + + Попытка + Причина = Сервер.ПоследняяОшибкаСети; + Адрес = Сервер.АдресДляСообщений(); + Исключение + Возврат ""; + КонецПопытки; + + Если НЕ ЗначениеЗаполнено(Причина) Тогда + Возврат ""; + КонецЕсли; + + Возврат СтрШаблон("Не удалось подключиться к %1: %2", Адрес, Причина); + +КонецФункции + +// Возвращаемое значение: +// Структура - Ответ (HTTPОтвет либо Неопределено), КодСостояния (Число, 0 — ответа не было), +// ОтказТранспорта (Строка - готовый текст отказа явно названного сервера). +Функция ЗапроситьПакет(Знач ПереченьСерверов, Знач ИмяСервера, Знач ИмяРесурса, Знач Спецификация) - ПакетУспешноПолучен = Ложь; ОтветСервера = Неопределено; + КодСостояния = 0; + ОтказТранспорта = ""; // Если указан нужный сервер, то только его и будем использовать Если ЗначениеЗаполнено(ИмяСервера) Тогда Сервер = ИндексСерверовПакетов[ИмяСервера]; - ОтветСервера = ЗапроситьПакетССервера(Сервер, ИмяРесурса); + Если Сервер = Неопределено Тогда + ВызватьИсключение СтрШаблон("Сервер пакетов <%1> не описан в настройках", ИмяСервера); + КонецЕсли; + ОтветСервера = ЗапроситьПакетССервера(Сервер, ИмяРесурса, Спецификация, КодСостояния); + + // сервер один и назван руками: до него не достучались — это и есть исход команды + Если ОтветСервера = Неопределено И КодСостояния = 0 Тогда + ОтказТранспорта = ОтказЕдинственногоСервера(Сервер); + КонецЕсли; Иначе @@ -72,11 +223,9 @@ Для Каждого ДоступныйСервер Из ПереченьСерверов Цикл Сервер = ИндексСерверовПакетов[ДоступныйСервер.Ключ]; - ОтветСервера = ЗапроситьПакетССервера(Сервер, ИмяРесурса); + ОтветСервера = ЗапроситьПакетССервера(Сервер, ИмяРесурса, Спецификация, КодСостояния); Если ОтветСервера <> Неопределено Тогда - Прервать; - КонецЕсли; КонецЦикла; @@ -84,24 +233,24 @@ КонецЕсли; Если ОтветСервера <> Неопределено Тогда - Лог.Отладка("Ресурс %1 успешно получен с %2", ИмяРесурса, Сервер.ПолучитьИмя()); - Возврат ОтветСервера; - КонецЕсли; - Возврат Неопределено; + Возврат Новый Структура("Ответ, КодСостояния, ОтказТранспорта", + ОтветСервера, КодСостояния, ОтказТранспорта); КонецФункции -Функция ЗапроситьПакетССервера(Сервер, ИмяРесурса) +Функция ЗапроситьПакетССервера(Сервер, ИмяРесурса, Знач Спецификация, КодСостояния = 0) ОтветСервера = Неопределено; Если Сервер.СерверДоступен() Тогда - ОтветСервера = Сервер.ПолучитьРесурс(ИмяРесурса); + ОтветСервера = Сервер.ПолучитьРесурс(ИмяРесурса, Спецификация); Если ОтветСервера <> Неопределено Тогда + КодСостояния = ОтветСервера.КодСостояния; + Если ОтветСервера.КодСостояния = 200 Тогда Возврат ОтветСервера; @@ -110,9 +259,11 @@ ОтветСервера.Закрыть(); - Лог.Информация("Ошибка подключения к хабу %1 <%2>", + // это ответ хаба, а не сбой связи: прежний текст «ошибка подключения» вводил в заблуждение + Лог.Отладка("Хаб %1 ответил кодом %2 на запрос %3", Сервер.ПолучитьИмя(), - ОтветСервера.КодСостояния); + КодСостояния, + ИмяРесурса); ОтветСервера = Неопределено; diff --git "a/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\234\320\265\320\275\320\265\320\264\320\266\320\265\321\200\320\243\321\201\321\202\320\260\320\275\320\276\320\262\320\272\320\270\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" "b/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\234\320\265\320\275\320\265\320\264\320\266\320\265\321\200\320\243\321\201\321\202\320\260\320\275\320\276\320\262\320\272\320\270\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" index f2eac85..f4bde72 100644 --- "a/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\234\320\265\320\275\320\265\320\264\320\266\320\265\321\200\320\243\321\201\321\202\320\260\320\275\320\276\320\262\320\272\320\270\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" +++ "b/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\234\320\265\320\275\320\265\320\264\320\266\320\265\321\200\320\243\321\201\321\202\320\260\320\275\320\276\320\262\320\272\320\270\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" @@ -18,6 +18,7 @@ Перем УстанавливатьЗависимости; Перем УстанавливатьЗависимостиРазработчика; Перем СоздаватьФайлыЗапуска; +Перем СерверТолькоДляКорневогоПакета; // BSLLS:NumberOfOptionalParams-off Процедура ПриСозданииОбъекта(Знач ВходящийРежимУстановкиПакетов = Неопределено, @@ -63,6 +64,18 @@ УстанавливатьЗависимостиРазработчика = Ложь; СоздаватьФайлыЗапуска = Истина; ИмяСервера = ВходящийИмяСервера; + СерверТолькоДляКорневогоПакета = Ложь; +КонецПроцедуры + +// Ограничивает закрепление сервера пакетов корневым пакетом: зависимости при этом +// разрешаются по всем описанным серверам. Нужно адресу пула в командной строке — +// в чужом пуле зависимостей может не быть. +// +// Параметры: +// ПСерверТолькоДляКорневогоПакета - Булево - Истина ограничивает закрепление. +// +Процедура ЗакрепитьСерверЗаКорневымПакетом(Знач ПСерверТолькоДляКорневогоПакета) Экспорт + СерверТолькоДляКорневогоПакета = ПСерверТолькоДляКорневогоПакета; КонецПроцедуры Процедура УстанавливатьЗависимости(Знач ПУстанавливатьЗависимости) Экспорт @@ -191,7 +204,12 @@ ВызватьИсключение "УровеньЗависимости не может быть меньше нуля"; КонецЕсли; - ФайлПакета = РаботаСПакетами.ПолучитьПакет(ИмяПакета, ВерсияПакета, , ИмяСервера); + СерверЗапроса = ИмяСервера; + Если СерверТолькоДляКорневогоПакета И УровеньЗависимости > 0 Тогда + СерверЗапроса = ""; + КонецЕсли; + + ФайлПакета = РаботаСПакетами.ПолучитьПакет(ИмяПакета, ВерсияПакета, , СерверЗапроса); УстановитьПакетИзАрхива(ФайлПакета, ЭтоЗависимыйПакет, УровеньЗависимости); КонецПроцедуры diff --git "a/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\236\320\272\321\200\321\203\320\266\320\265\320\275\320\270\320\265\320\237\321\200\320\276\321\206\320\265\321\201\321\201\320\260.os" "b/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\236\320\272\321\200\321\203\320\266\320\265\320\275\320\270\320\265\320\237\321\200\320\276\321\206\320\265\321\201\321\201\320\260.os" new file mode 100644 index 0000000..46cb78a --- /dev/null +++ "b/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\236\320\272\321\200\321\203\320\266\320\265\320\275\320\270\320\265\320\237\321\200\320\276\321\206\320\265\321\201\321\201\320\260.os" @@ -0,0 +1,40 @@ +// Переменные среды процесса. Отдельный объект, потому что от них зависят решения +// (какой PATH искать, есть ли вокруг конвейер) — в тестах он подменяется готовым набором. + +Перем Значения; + +Процедура ПриСозданииОбъекта(Знач ВходящиеЗначения = Неопределено) + + Значения = ?(ВходящиеЗначения = Неопределено, ПеременныеСреды(), ВходящиеЗначения); + +КонецПроцедуры + +// Значение переменной среды. +// +// Параметры: +// Имя - Строка - имя переменной. +// +// Возвращаемое значение: +// Строка - значение без краевых пробелов; пустая строка, если переменной нет. +// +Функция Значение(Знач Имя) Экспорт + + Прочитанное = Значения.Получить(Имя); + + Возврат ?(Прочитанное = Неопределено, "", СокрЛП(Строка(Прочитанное))); + +КонецФункции + +// Задана ли переменная среды непустым значением. +// +// Параметры: +// Имя - Строка - имя переменной. +// +// Возвращаемое значение: +// Булево. +// +Функция Задана(Знач Имя) Экспорт + + Возврат НЕ ПустаяСтрока(Значение(Имя)); + +КонецФункции diff --git "a/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\236\321\202\320\272\321\200\321\213\320\262\320\260\321\202\320\265\320\273\321\214\320\221\321\200\320\260\321\203\320\267\320\265\321\200\320\260.os" "b/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\236\321\202\320\272\321\200\321\213\320\262\320\260\321\202\320\265\320\273\321\214\320\221\321\200\320\260\321\203\320\267\320\265\321\200\320\260.os" new file mode 100644 index 0000000..c28ef59 --- /dev/null +++ "b/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\236\321\202\320\272\321\200\321\213\320\262\320\260\321\202\320\265\320\273\321\214\320\221\321\200\320\260\321\203\320\267\320\265\321\200\320\260.os" @@ -0,0 +1,155 @@ +// Открывает адрес в браузере пользователя: start на Windows, open на macOS, +// xdg-open в прочих Unix. Адрес приходит от хаба, поэтому до запуска он приводится +// к URL-кодировке и уезжает отдельным параметром, без командного процессора системы. + +Перем Запускатель; +Перем ИмяСемейства; + +Процедура ПриСозданииОбъекта(Знач ВходящийЗапускатель = Неопределено, Знач ВходящееСемейство = "") + + ИмяСемейства = ?(ПустаяСтрока(ВходящееСемейство), + СемействоПлатформы(), НРег(СокрЛП(ВходящееСемейство))); + Запускатель = ?(ВходящийЗапускатель = Неопределено, + Новый ЗапускательПрограмм(), ВходящийЗапускатель); + +КонецПроцедуры + +// Минимальная проверка адреса: открываем только http(s). Адрес приходит от хаба — +// возможно, чужого и по незашифрованному каналу, — а браузеру нельзя дать открыть +// file:// или javascript:. +// +// Параметры: +// Адрес - Строка - проверяемый адрес. +// +// Возвращаемое значение: +// Строка - причина отказа; пустая строка, если адрес годен. +// +Функция ПричинаОтказаВАдресе(Знач Адрес) Экспорт + + Значение = СокрЛП(Строка(Адрес)); + + Если ПустаяСтрока(Значение) Тогда + Возврат "Открывать нечего: адрес пуст"; + КонецЕсли; + + Если НЕ СтрНачинаетсяС(НРег(Значение), "http://") И НЕ СтрНачинаетсяС(НРег(Значение), "https://") Тогда + Возврат СтрШаблон("Хаб прислал адрес подтверждения <%1> не по http(s) — открывать не будем", + Значение); + КонецЕсли; + + Возврат ""; + +КонецФункции + +// Семейство платформы, по которому выбрана команда открытия. +// +// Возвращаемое значение: +// Строка - windows, macos либо unix. +// +Функция Семейство() Экспорт + + Возврат ИмяСемейства; + +КонецФункции + +// Команда открытия ссылки для текущей ОС. +// +// Возвращаемое значение: +// Строка - start, open либо xdg-open. +// +Функция КомандаОткрытия() Экспорт + + Если ИмяСемейства = "windows" Тогда + Возврат "start"; + ИначеЕсли ИмяСемейства = "macos" Тогда + Возврат "open"; + ИначеЕсли ИмяСемейства = "unix" Тогда + Возврат "xdg-open"; + КонецЕсли; + + ВызватьИсключение СтрШаблон("Неподдерживаемое семейство платформы <%1>", ИмяСемейства); + +КонецФункции + +// Открывает адрес командой текущей ОС. +// +// Параметры: +// Адрес - Строка - открываемый адрес. +// +// Возвращаемое значение: +// Структура - Успех (Булево), Инструмент (Строка - чем открыт), Сообщение (Строка - +// причина неудачи). +// +Функция Открыть(Знач Адрес) Экспорт + + Отказ = ПричинаОтказаВАдресе(Адрес); + Если НЕ ПустаяСтрока(Отказ) Тогда + Возврат Неудача(Отказ); + КонецЕсли; + + Команда = КомандаОткрытия(); + КодированныйАдрес = КодироватьСтроку(СокрЛП(Строка(Адрес)), СпособКодированияСтроки.URLВКодировкеURL); + Запуск = ЗапускОткрытия(КодированныйАдрес); + Результат = Запускатель.Запустить(Запуск.Исполняемое, Запуск.Параметры); + + Если Результат.КодВозврата = 0 Тогда + Возврат Новый Структура("Успех, Инструмент, Сообщение", Истина, Команда, ""); + КонецЕсли; + + Если ПустаяСтрока(Результат.Ошибка) Тогда + Причина = СтрШаблон("%1 завершился с кодом %2", Команда, + Формат(Результат.КодВозврата, "ЧГ=0; ЧН=0")); + Иначе + Причина = СтрШаблон("%1 не запустился: %2", Команда, Результат.Ошибка); + КонецЕсли; + + Возврат Неудача(Причина); + +КонецФункции + +/////////////////////////////////////////////////////////////////// + +Функция СемействоПлатформы() + + Платформа = Строка(Новый СистемнаяИнформация().ТипПлатформы); + + Если СтрНачинаетсяС(Платформа, "Windows") Тогда + Возврат "windows"; + ИначеЕсли СтрНачинаетсяС(Платформа, "MacOS") Тогда + Возврат "macos"; + КонецЕсли; + + Возврат "unix"; + +КонецФункции + +// На Windows start — встроенная команда cmd, поэтому запускается сам cmd, +// а пустые кавычки занимают место заголовка окна вместо адреса. +Функция ЗапускОткрытия(Знач Адрес) + + Параметры = Новый Массив; + + Если ИмяСемейства = "windows" Тогда + Параметры.Добавить("/c"); + Параметры.Добавить("start"); + Параметры.Добавить(ПустойЗаголовокОкна()); + Параметры.Добавить(Адрес); + Возврат Новый Структура("Исполняемое, Параметры", "cmd", Параметры); + КонецЕсли; + + Параметры.Добавить(Адрес); + + Возврат Новый Структура("Исполняемое, Параметры", КомандаОткрытия(), Параметры); + +КонецФункции + +// Две кавычки подряд: заголовок окна для start. +Функция ПустойЗаголовокОкна() + Возврат Символ(34) + Символ(34); +КонецФункции + +Функция Неудача(Знач Сообщение) + + Возврат Новый Структура("Успех, Инструмент, Сообщение", Ложь, "", Сообщение); + +КонецФункции diff --git "a/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\236\321\202\320\277\321\200\320\260\320\262\320\270\321\202\320\265\320\273\321\214\320\237\320\260\320\272\320\265\321\202\320\260.os" "b/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\236\321\202\320\277\321\200\320\260\320\262\320\270\321\202\320\265\320\273\321\214\320\237\320\260\320\272\320\265\321\202\320\260.os" new file mode 100644 index 0000000..6d81ebc --- /dev/null +++ "b/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\236\321\202\320\277\321\200\320\260\320\262\320\270\321\202\320\265\320\273\321\214\320\237\320\260\320\272\320\265\321\202\320\260.os" @@ -0,0 +1,164 @@ +// Транспорт публикации: отправка .ospx в хаб. + +#Использовать logos + +Перем Лог; + +Перем АдресСервера; +Перем Порт; +Перем Таймаут; +Перем БазовыеЗаголовки; + +Процедура ПриСозданииОбъекта(Знач ВходящийАдресСервера, Знач ВходящийПорт = 0, + Знач Заголовки = Неопределено, Знач ВходящийТаймаут = 600) + + АдресСервера = ВходящийАдресСервера; + Порт = ВходящийПорт; + Таймаут = ВходящийТаймаут; + БазовыеЗаголовки = ?(Заголовки = Неопределено, Новый Соответствие(), Заголовки); + +КонецПроцедуры + +// Публикует файл пакета в хаб. +// +// Параметры: +// РесурсПубликации - Строка - путь публикации на сервере. +// ФайлПакета - Строка - полный путь к файлу .ospx. +// Канал - Строка - значение заголовка CHANNEL. +// Принудительно - Булево - публиковать поверх уже занятого номера версии. +// +// Возвращаемое значение: +// Структура - Код (Число), Сообщение (Строка), Успех (Булево). +// +Функция Опубликовать(Знач РесурсПубликации, Знач ФайлПакета, Знач Канал, + Знач Принудительно = Ложь) Экспорт + + Файл = Новый Файл(ФайлПакета); + Заголовки = СобратьЗаголовки(); + Заголовки.Вставить("FILE-NAME", Файл.Имя); + Заголовки.Вставить("CHANNEL", Канал); + + Возврат ВыполнитьЗапрос(АдресПубликации(РесурсПубликации, Принудительно), Заголовки, + Новый ДвоичныеДанные(Файл.ПолноеИмя)); + +КонецФункции + +// Адрес публикации в запросе к хабу: маршрут пуша и, если публикуем принудительно, +// параметр force. Хаб, не знающий такого параметра, молча его пропустит — публикация +// в старый хаб остаётся прежней. +// +// Параметры: +// РесурсПубликации - Строка - путь публикации на сервере. +// Принудительно - Булево - публиковать поверх уже занятого номера версии. +// +// Возвращаемое значение: +// Строка - путь и, если публикуем принудительно, строка запроса. +// +Функция АдресПубликации(Знач РесурсПубликации, Знач Принудительно = Ложь) Экспорт + + Если НЕ Принудительно Тогда + Возврат РесурсПубликации; + КонецЕсли; + + Возврат СтрШаблон("%1%2force=1", РесурсПубликации, РазделительПараметра(РесурсПубликации)); + +КонецФункции + +/////////////////////////////////////////////////////////////////// + +// Маршрут публикации приходит из настроек сервера, а туда его пишет человек — со строкой +// запроса в том числе. Второй знак вопроса сделал бы force частью значения предыдущего параметра. +Функция РазделительПараметра(Знач Адрес) + + Возврат ?(СтрНайти(Адрес, "?") = 0, "?", "&"); + +КонецФункции + +Функция СобратьЗаголовки() + + Заголовки = Новый Соответствие(); + + Для Каждого КлючИЗначение Из БазовыеЗаголовки Цикл + Если ЗначениеЗаполнено(КлючИЗначение.Значение) Тогда + Заголовки.Вставить(КлючИЗначение.Ключ, + ОбщегоНазначенияOpm.ПрименитьПеременныеСреды(КлючИЗначение.Значение)); + КонецЕсли; + КонецЦикла; + + Возврат Заголовки; + +КонецФункции + +Функция ВыполнитьЗапрос(Знач Ресурс, Знач Заголовки, Знач Тело) + + Лог.Отладка("Публикация: %1%2", АдресСервера, Ресурс); + + // Порт = 0 означает «не задан»: платформа возьмёт его из схемы адреса + Если Порт = 0 Тогда + Соединение = Новый HTTPСоединение(АдресСервера, , , , , Таймаут); + Иначе + Соединение = Новый HTTPСоединение(АдресСервера, Порт, , , , Таймаут); + КонецЕсли; + Запрос = Новый HTTPЗапрос(Ресурс, Заголовки); + Запрос.УстановитьТелоИзДвоичныхДанных(Тело); + + Попытка + Ответ = Соединение.ОтправитьДляОбработки(Запрос); + Исключение + // адрес публикации всегда назван явно: до него не достучались — это отказ команды + ПодробноеОписание = ОписаниеОшибки(); + Лог.Отладка("Публикация в %1%2 не удалась: %3", АдресСервера, Ресурс, ПодробноеОписание); + ВызватьИсключение ОшибкиСети.НеУдалосьПодключиться(АдресДляСообщений(), ПодробноеОписание); + КонецПопытки; + + ТекстОтвета = Ответ.ПолучитьТелоКакСтроку(); + + Результат = Новый Структура(); + Результат.Вставить("Код", Ответ.КодСостояния); + Результат.Вставить("Сообщение", СообщениеХаба(ТекстОтвета)); + Результат.Вставить("Успех", УспешныйКод(Ответ.КодСостояния)); + + Лог.Отладка("Хаб ответил %1: %2", Ответ.КодСостояния, ТекстОтвета); + + Возврат Результат; + +КонецФункции + +// Достаёт человеческий текст из ответа хаба: JSON-конверт публикации пользователю не нужен. +Функция СообщениеХаба(Знач ТекстОтвета) + + Разобранное = ОбщегоНазначенияOpm.РазобратьJSONОбъект(ТекстОтвета); + + Если Разобранное = Неопределено Тогда + Возврат СокрЛП(Строка(ТекстОтвета)); + КонецЕсли; + + Значение = Разобранное.Получить("Сообщение"); + Если Значение <> Неопределено Тогда + Возврат СокрЛП(Строка(Значение)); + КонецЕсли; + + Ошибка = Разобранное.Получить("error"); + Если ТипЗнч(Ошибка) = Тип("Соответствие") И Ошибка.Получить("message") <> Неопределено Тогда + Возврат СокрЛП(Строка(Ошибка.Получить("message"))); + КонецЕсли; + + Возврат СокрЛП(Строка(ТекстОтвета)); + +КонецФункции + +Функция АдресДляСообщений() + + Возврат АдресаХаба.АдресСПортом(АдресСервера, Порт); + +КонецФункции + +Функция УспешныйКод(Знач КодСостояния) + + Возврат КодСостояния = 200 + ИЛИ КодСостояния = 201 + ИЛИ КодСостояния = 202; + +КонецФункции + +Лог = Логирование.ПолучитьЛог("oscript.app.opm"); diff --git "a/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\241\320\265\321\200\320\262\320\265\321\200\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" "b/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\241\320\265\321\200\320\262\320\265\321\200\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" index 368fe0a..42b228b 100644 --- "a/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\241\320\265\321\200\320\262\320\265\321\200\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" +++ "b/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\241\320\265\321\200\320\262\320\265\321\200\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" @@ -10,6 +10,10 @@ Перем Авторизация Экспорт; Перем ДополнительныеЗаголовки Экспорт; Перем Таймаут Экспорт; +// Истина у сервера, собранного по адресу из командной строки: до такого хаба +// «не достучались» — это отказ команды, а не повод молча идти к следующему зеркалу. +Перем НазванЯвно Экспорт; +Перем ПоследняяОшибкаСети Экспорт; Перем ПакетыХаба; @@ -26,9 +30,22 @@ Авторизация = ВходящаяАвторизация; ДополнительныеЗаголовки = Заголовки; Таймаут = ВходящийТаймаут; + НазванЯвно = Ложь; + ПоследняяОшибкаСети = ""; КонецПроцедуры +// Адрес сервера в том виде, в каком его называют пользователю. +// +// Возвращаемое значение: +// Строка - схема, хост и порт, если он не стандартный. +// +Функция АдресДляСообщений() Экспорт + + Возврат АдресаХаба.АдресСПортом(Сервер, Порт); + +КонецФункции + Функция ПолучитьИмя() Экспорт Возврат Имя; КонецФункции @@ -55,28 +72,85 @@ КонецФункции -// ИмяРесурса - имя файла относительно "Сервер/ПутьВХранилище" -// Возвращает HttpОтвет или Неопределено, если запрос вернул исключение. -Функция ПолучитьРесурс(Знач ИмяРесурса) Экспорт +// Получает ресурс с сервера. +// +// Параметры: +// ИмяРесурса - Строка - имя файла относительно "Сервер/ПутьНаСервере". +// Спецификация - Строка, Неопределено - то, что пользователь написал после @: точный номер +// версии, имя тега либо выражение версии. Уходит на хаб отдельным параметром +// запроса spec; Неопределено и пустая строка параметра не добавляют. +// +// Возвращаемое значение: +// HTTPОтвет - ответ хаба; Неопределено, если запрос завершился исключением. +// +Функция ПолучитьРесурс(Знач ИмяРесурса, Знач Спецификация = Неопределено) Экспорт Соединение = ИнициализироватьСоединение(); - Ресурс = ПутьНаСервере + ИмяРесурса; + Ресурс = АдресРесурса(ИмяРесурса, Спецификация); Запрос = Новый HTTPЗапрос(Ресурс); ДобавитьЗаголовки(Запрос); Попытка - + + ПоследняяОшибкаСети = ""; Возврат Соединение.Получить(Запрос); Исключение - Лог.Ошибка(ОписаниеОшибки()); + // служебная обёртка платформы уходит в отладку, наружу — только суть + ПодробноеОписание = ОписаниеОшибки(); + ПоследняяОшибкаСети = ОшибкиСети.Причина(ПодробноеОписание); + Лог.Отладка("Обращение к %1%2 не удалось: %3", Сервер, Ресурс, ПодробноеОписание); Возврат Неопределено; КонецПопытки; КонецФункции +// Адрес ресурса в запросе к хабу: путь выдачи с именем файла и спецификация +// отдельным параметром запроса. +// +// Параметры: +// ИмяРесурса - Строка - имя файла относительно "Сервер/ПутьНаСервере". +// Спецификация - Строка, Неопределено - строка после @; пустая даёт адрес без параметров. +// +// Возвращаемое значение: +// Строка - путь и, если спецификация названа, строка запроса. +// +Функция АдресРесурса(Знач ИмяРесурса, Знач Спецификация = Неопределено) Экспорт + + Адрес = ПутьНаСервере + ПутьВАдресе(ИмяРесурса); + + Если НЕ ЗначениеЗаполнено(Спецификация) Тогда + Возврат Адрес; + КонецЕсли; + + Возврат СтрШаблон("%1?spec=%2", Адрес, БезопасноеДляАдреса(Спецификация)); + +КонецФункции + +// Сегменты кодируются поодиночке: косая черта между ними — разделитель пути, а не символ имени. +Функция ПутьВАдресе(Знач ИмяРесурса) + + Сегменты = СтрРазделить(ИмяРесурса, "/", Истина); + + Для Индекс = 0 По Сегменты.ВГраница() Цикл + Сегменты[Индекс] = БезопасноеДляАдреса(Сегменты[Индекс]); + КонецЦикла; + + Возврат СтрСоединить(Сегменты, "/"); + +КонецФункции + +// КодировкаURL оставляет нетронутыми только незарезервированные символы, поэтому годится +// и сегменту пути, и значению параметра. На кодирование платформой полагаться нельзя: +// плюс она оставляет как есть, а в строке запроса он читается пробелом. +Функция БезопасноеДляАдреса(Знач Часть) + + Возврат КодироватьСтроку(Часть, СпособКодированияСтроки.КодировкаURL); + +КонецФункции + Функция ПрочитатьФайлСпискаПакетов(Текст) ТекстовыйДокумент = Новый ТекстовыйДокумент; @@ -101,15 +175,15 @@ Ответ = ПолучитьРесурс(Ресурс); Если Ответ = Неопределено Тогда - ТекстИсключения = "Ошибка подключения к зеркалу"; - ВызватьИсключение ТекстИсключения; + ВызватьИсключение ОшибкиСети.НеУдалосьПодключиться(АдресДляСообщений(), ПоследняяОшибкаСети); КонецЕсли; Если Ответ.КодСостояния <> 200 Тогда + КодСостояния = Ответ.КодСостояния; ТекстОтвета = Ответ.ПолучитьТелоКакСтроку(); - ТекстИсключения = СтрШаблон("Ошибка подключения к зеркалу код ответа: <%1> - |Текст ответа: <%2>", Ответ.КодСостояния, ТекстОтвета); - ВызватьИсключение ТекстИсключения; + Ответ.Закрыть(); + ВызватьИсключение СтрШаблон("%1 ответил кодом %2 на запрос списка пакетов: %3", + АдресДляСообщений(), Формат(КодСостояния, "ЧГ=0; ЧН=0"), СокрЛП(ТекстОтвета)); КонецЕсли; ТекстОтвета = Ответ.ПолучитьТелоКакСтроку(); Ответ.Закрыть(); @@ -127,15 +201,18 @@ Попытка ТекстОтвета = ПолучитьСписокПакетов("list.txt"); Исключение - Лог.Предупреждение( - СтрШаблон("Ошибка получения списка пакетов с хаба %1 по причине %2", - Имя, ОписаниеОшибки() - ) - ); + // Адрес, названный в команде руками, — единственный источник пакета: до него не + // достучались, значит команда провалилась. Перебор зеркал из конфигурации терпимее: + // недоступное зеркало пропускается в пользу следующего. + Если НазванЯвно Тогда + ВызватьИсключение ОшибкиСети.Суть(ОписаниеОшибки()); + КонецЕсли; + Лог.Предупреждение("Список пакетов зеркала %1 не получен: %2", + Имя, ОшибкиСети.Суть(ОписаниеОшибки())); КонецПопытки; ПрочитатьФайлСпискаПакетов(ТекстОтвета); - + Возврат ПакетыХаба; КонецФункции diff --git "a/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\242\320\276\320\272\320\265\320\275\320\232\320\276\320\275\320\262\320\265\320\271\320\265\321\200\320\260CI.os" "b/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\242\320\276\320\272\320\265\320\275\320\232\320\276\320\275\320\262\320\265\320\271\320\265\321\200\320\260CI.os" new file mode 100644 index 0000000..e22287f --- /dev/null +++ "b/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\242\320\276\320\272\320\265\320\275\320\232\320\276\320\275\320\262\320\265\320\271\320\265\321\200\320\260CI.os" @@ -0,0 +1,241 @@ +// Достаёт OIDC id-token у окружения сборочного конвейера, чтобы публиковать без PAT. +// GitHub Actions выдаёт токен своим сервисом токенов, GitLab CI кладёт его в переменную +// среды (по соглашению id_token либо названную опцией <--oidc-token-var>). +// Подпись токена проверяет хаб: клиент только получает значение и отдаёт его как есть. + +Перем Окружение; +Перем Запросчик; + +Процедура ПриСозданииОбъекта(Знач ВходящееОкружение = Неопределено, Знач ВходящийЗапросчик = Неопределено) + + Окружение = ?(ВходящееОкружение = Неопределено, Новый ОкружениеПроцесса(), ВходящееОкружение); + Запросчик = ?(ВходящийЗапросчик = Неопределено, Новый ЗапросчикHTTP(), ВходящийЗапросчик); + +КонецПроцедуры + +// Имена переменных среды, в которых id-token ищется, когда имя не названо опцией. +// GitLab CI имя переменной назначает сам, поэтому опереться можно только на соглашение. +// +// Возвращаемое значение: +// Массив из Строка. +// +Функция ИменаПеременныхПоУмолчанию() Экспорт + + Имена = Новый Массив(); + Имена.Добавить("id_token"); + Имена.Добавить("ID_TOKEN"); + + Возврат Имена; + +КонецФункции + +// Что вокруг говорит о доверенной публикации: набор переменных среды и решает, +// публиковать id-token'ом или обычным токеном. Причина возвращается в обе стороны — +// найденное объясняется человеку, ненайденное уходит в отладочный журнал. +// +// Параметры: +// ИмяПеременной - Строка - имя переменной среды, названное опцией --oidc-token-var. +// +// Возвращаемое значение: +// Структура - Найдено (Булево), Способ (Строка - переменная либо сервис), +// ИмяПеременной (Строка), Причина (Строка). +// +Функция Обнаружить(Знач ИмяПеременной = "") Экспорт + + Названная = СокрЛП(Строка(ИмяПеременной)); + + Если НЕ ПустаяСтрока(Названная) Тогда + Если Окружение.Задана(Названная) Тогда + Возврат Найдено("переменная", Названная, СтрШаблон( + "переменная среды %1 названа опцией <--oidc-token-var> и заполнена", Названная)); + КонецЕсли; + Возврат НеНайдено(СтрШаблон("переменная среды %1, названная опцией <--oidc-token-var>," + + " не задана либо пуста", Названная)); + КонецЕсли; + + Для Каждого Имя Из ИменаПеременныхПоУмолчанию() Цикл + Если Окружение.Задана(Имя) Тогда + Возврат Найдено("переменная", Имя, + СтрШаблон("задана переменная среды %1 с id-token конвейера", Имя)); + КонецЕсли; + КонецЦикла; + + Если Окружение.Задана("ACTIONS_ID_TOKEN_REQUEST_URL") + И Окружение.Задана("ACTIONS_ID_TOKEN_REQUEST_TOKEN") Тогда + Возврат Найдено("сервис", "", + "GitHub Actions выдал конвейеру доступ к сервису id-token (ACTIONS_ID_TOKEN_REQUEST_URL)"); + КонецЕсли; + + Если Окружение.Задана("GITHUB_ACTIONS") Тогда + Возврат НеНайдено("вокруг GitHub Actions, но id-token конвейеру не выдан —" + + " в workflow нужно permissions: id-token: write"); + КонецЕсли; + + Если Окружение.Задана("GITLAB_CI") Тогда + Возврат НеНайдено("вокруг GitLab CI, но переменной с id-token не видно: опишите секцию" + + " id_tokens в .gitlab-ci.yml (aud — адрес хаба) и назовите переменную id_token" + + " либо укажите её имя опцией <--oidc-token-var>"); + КонецЕсли; + + Возврат НеНайдено("вокруг не видно ни переменной id_token, ни конвейера с поддержкой id-token"); + +КонецФункции + +// Получает id-token конвейера для указанной аудитории. +// +// Параметры: +// Аудитория - Строка - адрес инстанса хаба: хаб сверяет клейм aud именно с ним. +// ИмяПеременной - Строка - имя переменной среды с готовым id-token (GitLab CI). +// +// Возвращаемое значение: +// Структура - Успех (Булево), Токен (Строка), Источник (Строка - откуда взят), +// Сообщение (Строка - причина неудачи). +// +Функция Получить(Знач Аудитория, Знач ИмяПеременной = "") Экспорт + + Если ПустаяСтрока(Аудитория) Тогда + Возврат Отказ("", "не задана аудитория id-token, а хаб сверяет клейм aud со своим адресом"); + КонецЕсли; + + Если НЕ ПустаяСтрока(ИмяПеременной) Тогда + Возврат ИзПеременнойСреды(ИмяПеременной); + КонецЕсли; + + Обнаруженное = Обнаружить(); + + Если Обнаруженное.Найдено И Обнаруженное.Способ = "переменная" Тогда + Возврат ИзПеременнойСреды(Обнаруженное.ИмяПеременной); + КонецЕсли; + + Если Обнаруженное.Найдено Тогда + Возврат ИзСервисаGitHub(Аудитория); + КонецЕсли; + + Возврат Отказ("", Обнаруженное.Причина); + +КонецФункции + +/////////////////////////////////////////////////////////////////// + +Функция ИзПеременнойСреды(Знач ИмяПеременной) + + Источник = СтрШаблон("переменная среды %1", ИмяПеременной); + Значение = Окружение.Значение(ИмяПеременной); + + Если ПустаяСтрока(Значение) Тогда + Возврат Отказ(Источник, СтрШаблон("переменная среды %1 пуста — опишите её в секции id_tokens" + + " конвейера, указав aud, равный адресу хаба", ИмяПеременной)); + КонецЕсли; + + Если НЕ ПохожеНаТокенКонвейера(Значение) Тогда + Возврат Отказ(Источник, СтрШаблон("значение переменной среды %1 не похоже на id-token:" + + " ожидался JWT из трёх сегментов через точку", ИмяПеременной)); + КонецЕсли; + + Возврат Успех(Значение, Источник); + +КонецФункции + +Функция ИзСервисаGitHub(Знач Аудитория) + + Источник = "GitHub Actions"; + ТокенЗапроса = Окружение.Значение("ACTIONS_ID_TOKEN_REQUEST_TOKEN"); + + Если ПустаяСтрока(ТокенЗапроса) Тогда + Возврат Отказ(Источник, "в окружении есть ACTIONS_ID_TOKEN_REQUEST_URL, но нет" + + " ACTIONS_ID_TOKEN_REQUEST_TOKEN — запросить id-token нечем"); + КонецЕсли; + + Заголовки = Новый Соответствие(); + Заголовки.Вставить("Authorization", "Bearer " + ТокенЗапроса); + Заголовки.Вставить("Accept", "application/json"); + + Ответ = Запросчик.Получить( + САудиторией(Окружение.Значение("ACTIONS_ID_TOKEN_REQUEST_URL"), Аудитория), Заголовки); + + Если Ответ.Код = 0 Тогда + Возврат Отказ(Источник, СтрШаблон("сервис токенов GitHub недоступен: %1", Ответ.Тело)); + КонецЕсли; + + Если Ответ.Код = 403 Тогда + Возврат Отказ(Источник, "GitHub отказал в выдаче id-token — в workflow нужно" + + " permissions: id-token: write"); + КонецЕсли; + + Если Ответ.Код <> 200 Тогда + Возврат Отказ(Источник, СтрШаблон("сервис токенов GitHub ответил кодом %1", + Формат(Ответ.Код, "ЧГ=0; ЧН=0"))); + КонецЕсли; + + Значение = ЗначениеТокенаИзОтвета(Ответ.Тело); + + Если ПустаяСтрока(Значение) Тогда + Возврат Отказ(Источник, "в ответе сервиса токенов GitHub нет поля value"); + КонецЕсли; + + Возврат Успех(Значение, Источник); + +КонецФункции + +// audience обязателен: хаб сверяет клейм aud и отказывает при несовпадении. +Функция САудиторией(Знач АдресСервиса, Знач Аудитория) + + Разделитель = ?(СтрНайти(АдресСервиса, "?") > 0, "&", "?"); + + Возврат СтрШаблон("%1%2audience=%3", АдресСервиса, Разделитель, + КодироватьСтроку(Аудитория, СпособКодированияСтроки.КодировкаURL)); + +КонецФункции + +Функция ЗначениеТокенаИзОтвета(Знач Тело) + + Разобранное = ОбщегоНазначенияOpm.РазобратьJSONОбъект(Тело); + Значение = ?(Разобранное = Неопределено, Неопределено, Разобранное.Получить("value")); + + Возврат ?(Значение = Неопределено, "", СокрЛП(Строка(Значение))); + +КонецФункции + +// Форма компактного JWS: три сегмента через точку. Подпись не проверяем — это дело хаба. +Функция ПохожеНаТокенКонвейера(Знач Значение) + + Части = СтрРазделить(Значение, ".", Истина); + + Если Части.Количество() <> 3 Тогда + Возврат Ложь; + КонецЕсли; + + Для Каждого Часть Из Части Цикл + Если ПустаяСтрока(Часть) Тогда + Возврат Ложь; + КонецЕсли; + КонецЦикла; + + Возврат Истина; + +КонецФункции + +Функция Найдено(Знач Способ, Знач ИмяПеременной, Знач Причина) + + Возврат Новый Структура("Найдено, Способ, ИмяПеременной, Причина", + Истина, Способ, ИмяПеременной, Причина); + +КонецФункции + +Функция НеНайдено(Знач Причина) + + Возврат Новый Структура("Найдено, Способ, ИмяПеременной, Причина", Ложь, "", "", Причина); + +КонецФункции + +Функция Успех(Знач Токен, Знач Источник) + + Возврат Новый Структура("Успех, Токен, Источник, Сообщение", Истина, Токен, Источник, ""); + +КонецФункции + +Функция Отказ(Знач Источник, Знач Сообщение) + + Возврат Новый Структура("Успех, Токен, Источник, Сообщение", Ложь, "", Источник, Сообщение); + +КонецФункции diff --git "a/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\244\320\260\320\271\320\273\320\276\320\262\321\213\320\271\320\241\320\265\321\200\320\262\320\265\321\200\320\236\321\202\320\262\320\265\321\202.os" "b/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\244\320\260\320\271\320\273\320\276\320\262\321\213\320\271\320\241\320\265\321\200\320\262\320\265\321\200\320\236\321\202\320\262\320\265\321\202.os" index 9df20eb..7d1ab03 100644 --- "a/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\244\320\260\320\271\320\273\320\276\320\262\321\213\320\271\320\241\320\265\321\200\320\262\320\265\321\200\320\236\321\202\320\262\320\265\321\202.os" +++ "b/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\244\320\260\320\271\320\273\320\276\320\262\321\213\320\271\320\241\320\265\321\200\320\262\320\265\321\200\320\236\321\202\320\262\320\265\321\202.os" @@ -2,20 +2,25 @@ Перем Лог; Перем КодСостояния Экспорт; +Перем Заголовки Экспорт; Перем ИмяФайла; -Процедура ПриСозданииОбъекта(Знач ПутьКФайлу) - +// ПодборВерсииРазрешён - Ложь запрещает подставлять старшую версию каталога вместо +// ненайденного файла. Так отвечают на запрос с названной спецификацией: подобрать её +// каталогу нечем, и подстановка была бы молчаливой подменой версии. +Процедура ПриСозданииОбъекта(Знач ПутьКФайлу, Знач ПодборВерсииРазрешён = Истина) + ИмяФайла = ПутьКФайлу; - + Заголовки = Новый Соответствие(); + Файл = Новый Файл(ПутьКФайлу); Если Файл.Существует() Тогда КодСостояния = 200; Иначе - + КодСостояния = 404; - - Если СтрЗаканчиваетсяНа(ПутьКФайлу, ".ospx") Тогда + + Если ПодборВерсииРазрешён И СтрЗаканчиваетсяНа(ПутьКФайлу, ".ospx") Тогда ПодобратьФайлПакета(Файл.Путь); КонецЕсли; diff --git "a/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\244\320\260\320\271\320\273\320\276\320\262\321\213\320\271\320\241\320\265\321\200\320\262\320\265\321\200\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" "b/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\244\320\260\320\271\320\273\320\276\320\262\321\213\320\271\320\241\320\265\321\200\320\262\320\265\321\200\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" index fceeaea..8c4eb69 100644 --- "a/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\244\320\260\320\271\320\273\320\276\320\262\321\213\320\271\320\241\320\265\321\200\320\262\320\265\321\200\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" +++ "b/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\244\320\260\320\271\320\273\320\276\320\262\321\213\320\271\320\241\320\265\321\200\320\262\320\265\321\200\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" @@ -33,17 +33,21 @@ // // Параметры: // ИмяРесурса - Строка - имя файла относительно "Сервер/ПутьВХранилище" +// Спецификация - Строка, Неопределено - строка после @. Каталог разрешать её не умеет, +// поэтому названная спецификация запрещает подбор версии: нет файла — 404, +// а не молчаливая подмена старшей версией каталога // // Возвращаемое значение: // ФайловыйСерверОтвет - Если файл ресурса найден // Неопределено - Если файл ресурса не найден или возникла ошибка обращения к файлам -Функция ПолучитьРесурс(Знач ИмяРесурса) Экспорт +Функция ПолучитьРесурс(Знач ИмяРесурса, Знач Спецификация = Неопределено) Экспорт ПутьКРесурсу = ОбъединитьПути(КорневойКаталогПоиска, ИмяРесурса); + ПодборВерсииРазрешён = (Спецификация = Неопределено); Попытка - - Возврат Новый ФайловыйСерверОтвет(ПутьКРесурсу); + + Возврат Новый ФайловыйСерверОтвет(ПутьКРесурсу, ПодборВерсииРазрешён); Исключение diff --git "a/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\220\320\264\321\200\320\265\321\201\320\260\320\245\320\260\320\261\320\260.os" "b/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\220\320\264\321\200\320\265\321\201\320\260\320\245\320\260\320\261\320\260.os" new file mode 100644 index 0000000..283a814 --- /dev/null +++ "b/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\220\320\264\321\200\320\265\321\201\320\260\320\245\320\260\320\261\320\260.os" @@ -0,0 +1,466 @@ +/////////////////////////////////////////////////////////////////// +// +// Разбор адресов хаба, указанных прямо в командной строке: +// [схема://]хост[:порт][/пул[/пакет[@спецификация]]] +// +////////////////////////////////////////////////////////////////// + +#Использовать logos + +Перем Лог; + +// Похожа ли строка на адрес хаба, а не на имя пакета либо маску файла. +// Адресом делает явная схема, форма хост:порт либо разделитель пути при первом сегменте, +// похожем на хост. Хост без пула — полноценный адрес: он означает пул хаба по умолчанию. +// +// Параметры: +// Значение - Строка - разбираемая строка. +// +// Возвращаемое значение: +// Булево - Истина, если строку следует разбирать как адрес хаба. +// +Функция ЭтоАдресХаба(Знач Значение) Экспорт + + Строка_ = СокрЛП(Строка(Значение)); + + // файл пакета выигрывает у адреса всегда: расширение однозначно + Если ПустаяСтрока(Строка_) ИЛИ СтрЗаканчиваетсяНа(НРег(Строка_), ".ospx") Тогда + Возврат Ложь; + КонецЕсли; + + Если ЕстьСхема(Строка_) Тогда + Возврат Истина; + КонецЕсли; + + Сегменты = ЗначащиеСегменты(ОтрезатьПосле(ОтрезатьПосле(Строка_, "?"), "#")); + + Если Сегменты.Количество() = 0 Тогда + Возврат Ложь; + КонецЕсли; + + // Без разделителя пути адресом делает только явный порт: ни имя пакета, ни маска файла + // двоеточия с числом не содержат, а голое доменное имя от них не отличить. + Если СтрНайти(Строка_, "/") = 0 Тогда + Возврат ЕстьПортВАвторитете(Сегменты[0]); + КонецЕсли; + + Возврат ПохожеНаХост(Сегменты[0]); + +КонецФункции + +// Разбирает адрес, в котором последний сегмент — имя пакета. +// Один сегмент после хоста означает пакет в пуле по умолчанию, два — пул и пакет: +// пул сам по себе на установке бесполезен, поэтому неоднозначность снимается ролью адреса. +// +// Параметры: +// Значение - Строка - адрес вида [схема://]хост[:порт]/[пул/]пакет[@спецификация]. +// +// Возвращаемое значение: +// Структура - то же, что у Разобрать(); Пул пуст для пула по умолчанию. +// +// Выбрасывает исключение, если имя пакета в адресе не указано. +// +Функция РазобратьАдресПакета(Знач Значение) Экспорт + + Результат = Разобрать(Значение); + + Если ЗначениеЗаполнено(Результат.ИмяПакета) Тогда + Возврат Результат; + КонецЕсли; + + Если ПустаяСтрока(Результат.Пул) Тогда + ВызватьИсключение СтрШаблон( + "Адрес установки <%1>: не указано имя пакета. Ожидалось хост[:порт]/[пул/]пакет[@спецификация]", + Значение); + КонецЕсли; + + // единственный сегмент после хоста — это пакет в пуле по умолчанию, а не пул + ИмяВерсия = РаботаСВерсиями.РазобратьИмяПакета(Результат.Пул); + Результат.ИмяПакета = ИмяВерсия.ИмяПакета; + Результат.Спецификация = ИмяВерсия.Версия; + Результат.Пул = ""; + + Возврат Результат; + +КонецФункции + +// Разбирает адрес хаба на составные части. +// +// Параметры: +// Значение - Строка - адрес вида [схема://]хост[:порт][/пул[/пакет[@спецификация]]]. +// +// Возвращаемое значение: +// Структура - Схема (Строка), Хост (Строка), Порт (Число, 0 — порт по схеме), +// БазовыйАдрес (Строка), Пул (Строка, пустой — пул по умолчанию), +// ИмяПакета (Строка), +// Спецификация (Строка либо Неопределено: Неопределено — символа @ в адресе +// не было и версию выбирает хаб, пустая строка — @ написан, +// а спецификация за ним нет, и это отказ, а не выбор хаба), +// СхемаЯвная (Булево), ПортЯвный (Булево), ХостЛокальный (Булево). +// +// Выбрасывает исключение при пустом адресе, неизвестной схеме, нечисловом порте +// и лишних сегментах пути. +// +Функция Разобрать(Знач Значение) Экспорт + + Исходный = СокрЛП(Строка(Значение)); + + Если ПустаяСтрока(Исходный) Тогда + ВызватьИсключение "Пустой адрес хаба"; + КонецЕсли; + + Схема = ""; + Остаток = Исходный; + + ПозицияСхемы = СтрНайти(Исходный, "://"); + Если ПозицияСхемы > 0 Тогда + Схема = НРег(Лев(Исходный, ПозицияСхемы - 1)); + Остаток = Сред(Исходный, ПозицияСхемы + 3); + Если Схема <> "http" И Схема <> "https" Тогда + ВызватьИсключение СтрШаблон("Адрес хаба <%1>: поддерживаются только схемы http и https", Исходный); + КонецЕсли; + КонецЕсли; + + // хвост после ? и # к адресу хаба отношения не имеет + Остаток = ОтрезатьПосле(ОтрезатьПосле(Остаток, "?"), "#"); + + Сегменты = ЗначащиеСегменты(Остаток); + + Если Сегменты.Количество() = 0 Тогда + ВызватьИсключение СтрШаблон("Адрес хаба <%1>: не указан хост", Исходный); + КонецЕсли; + + Авторитет = РазобратьАвторитет(Сегменты[0], Исходный); + + Пул = ""; + ИмяПакета = ""; + Спецификация = Неопределено; + + Если Сегменты.Количество() > 3 Тогда + ВызватьИсключение СтрШаблон( + "Адрес хаба <%1>: ожидалось хост[:порт][/пул[/пакет[@спецификация]]]", Исходный); + КонецЕсли; + + Если Сегменты.Количество() >= 2 Тогда + Пул = Сегменты[1]; + КонецЕсли; + + Если Сегменты.Количество() = 3 Тогда + ИмяВерсия = РаботаСВерсиями.РазобратьИмяПакета(Сегменты[2]); + ИмяПакета = ИмяВерсия.ИмяПакета; + Спецификация = ИмяВерсия.Версия; + Если ПустаяСтрока(ИмяПакета) Тогда + ВызватьИсключение СтрШаблон("Адрес хаба <%1>: не указано имя пакета", Исходный); + КонецЕсли; + КонецЕсли; + + ХостЛокальный = ЭтоЛокальныйХост(Авторитет.Хост); + СхемаЯвная = ЗначениеЗаполнено(Схема); + + Если НЕ СхемаЯвная Тогда + // Умолчание: http для локального хоста и для адреса с явным портом, иначе https. + // Публичное имя без порта не понижаем до открытого канала — по нему уходит токен. + Схема = ?(ХостЛокальный ИЛИ Авторитет.ПортЯвный, "http", "https"); + КонецЕсли; + + Возврат Новый Структура( + "Схема, Хост, Порт, БазовыйАдрес, Пул, ИмяПакета, Спецификация, СхемаЯвная, ПортЯвный, ХостЛокальный", + Схема, Авторитет.Хост, Авторитет.Порт, + СобратьБазовыйАдрес(Схема, Авторитет.Хост, Авторитет.Порт), + Пул, ИмяПакета, Спецификация, СхемаЯвная, Авторитет.ПортЯвный, ХостЛокальный); + +КонецФункции + +// Порт с учётом порта по умолчанию для схемы. +// +// Параметры: +// Схема - Строка - http либо https. +// Порт - Число - порт адреса; 0 — порт не задан. +// +// Возвращаемое значение: +// Число - заданный порт либо порт схемы. +// +Функция ЭффективныйПорт(Знач Схема, Знач Порт) Экспорт + + Если Порт <> 0 Тогда + Возврат Порт; + КонецЕсли; + + Возврат ?(Схема = "https", 443, 80); + +КонецФункции + +// Адрес с портом для сообщений пользователю: порт дописывается, только если +// он не подразумевается схемой и не указан в самом адресе. +// +// Параметры: +// Адрес - Строка - адрес сервера, возможно со схемой. +// Порт - Число - порт; 0 ничего не дописывает. +// +// Возвращаемое значение: +// Строка. +// +Функция АдресСПортом(Знач Адрес, Знач Порт) Экспорт + + Если Порт = 0 ИЛИ СтрЗаканчиваетсяНа(Адрес, ":" + Формат(Порт, "ЧГ=0; ЧН=0")) Тогда + Возврат Адрес; + КонецЕсли; + + Если (СтрНачинаетсяС(НРег(Адрес), "http://") И Порт = 80) + ИЛИ (СтрНачинаетсяС(НРег(Адрес), "https://") И Порт = 443) Тогда + Возврат Адрес; + КонецЕсли; + + Возврат СтрШаблон("%1:%2", Адрес, Формат(Порт, "ЧГ=0; ЧН=0")); + +КонецФункции + +// Путь выдачи пакетов пула для поля ПутьНаСервере сервера пакетов. +// Спецификация после @ на путь не влияет: она уходит на хаб параметром spec. +// +// Параметры: +// Пул - Строка - имя пула; пустое даёт легаси-путь пула по умолчанию. +// +// Возвращаемое значение: +// Строка - путь, оканчивающийся косой чертой. +// +Функция ПутьВыдачи(Знач Пул) Экспорт + + ИмяПула = СокрЛП(Строка(Пул)); + + // имя пула по умолчанию — настройка хаба, клиенту оно неизвестно: остаётся легаси-выдача + Если ПустаяСтрока(ИмяПула) Тогда + Возврат "/download/"; + КонецЕсли; + + Возврат СтрШаблон("/api/v1/pools/%1/download/", ИмяПула); + +КонецФункции + +// Путь публикации пула для поля РесурсПубликацииПакетов сервера пакетов. +// +// Параметры: +// Пул - Строка - имя пула; пустое даёт легаси-путь /push. +// +// Возвращаемое значение: +// Строка - путь публикации. +// +Функция ПутьПубликации(Знач Пул) Экспорт + + ИмяПула = СокрЛП(Строка(Пул)); + + Если ПустаяСтрока(ИмяПула) Тогда + Возврат "/push"; + КонецЕсли; + + Возврат СтрШаблон("/api/v1/pools/%1/push", ИмяПула); + +КонецФункции + +// Имя пула, в который ведёт путь публикации сервера из настроек. +// Обратная сторона ПутьПубликации(): по описанному в opm.cfg серверу видно, публикуем +// мы в пул OpenHub или на легаси-маршрут, где пула нет и канал приходится определять. +// +// Параметры: +// ПутьПубликации - Строка - значение РесурсПубликацииПакетов. +// +// Возвращаемое значение: +// Строка - имя пула; пустая строка для легаси-маршрута. +// +Функция ПулИзПутиПубликации(Знач ПутьПубликации) Экспорт + + Значение = СокрЛП(Строка(ПутьПубликации)); + Префикс = "/api/v1/pools/"; + + Если НЕ СтрНачинаетсяС(НРег(Значение), Префикс) Тогда + Возврат ""; + КонецЕсли; + + Сегменты = СтрРазделить(Сред(Значение, СтрДлина(Префикс) + 1), "/", Ложь); + + Возврат ?(Сегменты.Количество() = 0, "", СокрЛП(Сегменты[0])); + +КонецФункции + +// Имя сервера пакетов, под которым адрес из командной строки живёт в настройках прогона. +// +// Параметры: +// РазобранныйАдрес - Структура - результат Разобрать(). +// +// Возвращаемое значение: +// Строка - имя сервера для настроек и сообщений. +// +Функция ИмяСервераДляАдреса(Знач РазобранныйАдрес) Экспорт + + Если ПустаяСтрока(РазобранныйАдрес.Пул) Тогда + Возврат РазобранныйАдрес.БазовыйАдрес; + КонецЕсли; + + Возврат СтрШаблон("%1/%2", РазобранныйАдрес.БазовыйАдрес, РазобранныйАдрес.Пул); + +КонецФункции + +// Предупреждает, если секрет уйдёт по открытому каналу на нелокальный хост. +// +// Параметры: +// РазобранныйАдрес - Структура - результат Разобрать(). +// +Процедура ПредупредитьОбОткрытомКанале(Знач РазобранныйАдрес) Экспорт + + Если РазобранныйАдрес.Схема <> "http" ИЛИ РазобранныйАдрес.ХостЛокальный Тогда + Возврат; + КонецЕсли; + + Лог.Предупреждение( + "Адрес %1 использует открытый http: токен уйдёт по незашифрованному каналу. Укажите https:// явно, если хаб его поддерживает.", + РазобранныйАдрес.БазовыйАдрес); + +КонецПроцедуры + +/////////////////////////////////////////////////////////////////// + +// Хостом считаем localhost и его поддомены, IPv6 в скобках, адрес с явным портом +// и всё, где есть точка (доменное имя либо IPv4). Короткое имя без точки и без порта +// от каталога в относительном пути не отличить — такой адрес требует явной схемы. +Функция ПохожеНаХост(Знач Авторитет) + + Если ЕстьПортВАвторитете(Авторитет) Тогда + Возврат Истина; + КонецЕсли; + + Ниж = НРег(Авторитет); + + Возврат Ниж = "localhost" + ИЛИ СтрЗаканчиваетсяНа(Ниж, ".localhost") + ИЛИ СтрНачинаетсяС(Ниж, "[") + ИЛИ СтрНайти(Ниж, ".") > 0; + +КонецФункции + +// Есть ли в авторитете явный порт: двоеточие, после которого до конца только цифры. +Функция ЕстьПортВАвторитете(Знач Авторитет) + + Позиция = СтрНайти(Авторитет, ":", НаправлениеПоиска.СКонца); + + Возврат Позиция > 0 И ЭтоЦифры(Сред(Авторитет, Позиция + 1)); + +КонецФункции + +Функция ЕстьСхема(Знач Значение) + Ниж = НРег(Значение); + Возврат СтрНачинаетсяС(Ниж, "http://") ИЛИ СтрНачинаетсяС(Ниж, "https://"); +КонецФункции + +Функция ОтрезатьПосле(Знач Значение, Знач Разделитель) + Позиция = СтрНайти(Значение, Разделитель); + Возврат ?(Позиция = 0, Значение, Лев(Значение, Позиция - 1)); +КонецФункции + +Функция ЗначащиеСегменты(Знач Путь) + + Результат = Новый Массив(); + Для Каждого Сегмент Из СтрРазделить(Путь, "/", Ложь) Цикл + Если НЕ ПустаяСтрока(СокрЛП(Сегмент)) Тогда + Результат.Добавить(СокрЛП(Сегмент)); + КонецЕсли; + КонецЦикла; + + Возврат Результат; + +КонецФункции + +Функция РазобратьАвторитет(Знач Авторитет, Знач Исходный) + + Хост = Авторитет; + Порт = 0; + ПортЯвный = Ложь; + + Если СтрНачинаетсяС(Хост, "[") Тогда + ПозицияСкобки = СтрНайти(Хост, "]"); + Если ПозицияСкобки = 0 Тогда + ВызватьИсключение СтрШаблон("Адрес хаба <%1>: не закрыта скобка IPv6-хоста", Исходный); + КонецЕсли; + ХвостПослеСкобки = Сред(Хост, ПозицияСкобки + 1); + Хост = Лев(Хост, ПозицияСкобки); + Если СтрНачинаетсяС(ХвостПослеСкобки, ":") Тогда + Порт = РазобратьПорт(Сред(ХвостПослеСкобки, 2), Исходный); + ПортЯвный = Истина; + КонецЕсли; + ИначеЕсли СтрНайти(Хост, ":") > 0 Тогда + Части = СтрРазделить(Хост, ":", Истина); + Если Части.Количество() <> 2 Тогда + ВызватьИсключение СтрШаблон("Адрес хаба <%1>: неразобранный хост <%2>", Исходный, Авторитет); + КонецЕсли; + Хост = Части[0]; + Порт = РазобратьПорт(Части[1], Исходный); + ПортЯвный = Истина; + КонецЕсли; + + Если ПустаяСтрока(Хост) Тогда + ВызватьИсключение СтрШаблон("Адрес хаба <%1>: не указан хост", Исходный); + КонецЕсли; + + Возврат Новый Структура("Хост, Порт, ПортЯвный", Хост, Порт, ПортЯвный); + +КонецФункции + +Функция РазобратьПорт(Знач Значение, Знач Исходный) + + Если ПустаяСтрока(Значение) Тогда + ВызватьИсключение СтрШаблон("Адрес хаба <%1>: пустой номер порта", Исходный); + КонецЕсли; + + Если НЕ ЭтоЦифры(Значение) Тогда + ВызватьИсключение СтрШаблон("Адрес хаба <%1>: порт <%2> не число", Исходный, Значение); + КонецЕсли; + + Возврат Число(Значение); + +КонецФункции + +Функция ЭтоЦифры(Знач Значение) + + Если ПустаяСтрока(Значение) Тогда + Возврат Ложь; + КонецЕсли; + + Для Индекс = 1 По СтрДлина(Значение) Цикл + Если СтрНайти("0123456789", Сред(Значение, Индекс, 1)) = 0 Тогда + Возврат Ложь; + КонецЕсли; + КонецЦикла; + + Возврат Истина; + +КонецФункции + +Функция СобратьБазовыйАдрес(Знач Схема, Знач Хост, Знач Порт) + + Результат = СтрШаблон("%1://%2", Схема, Хост); + + Если Порт <> 0 И НЕ ПортПоУмолчанию(Схема, Порт) Тогда + Результат = СтрШаблон("%1:%2", Результат, Формат(Порт, "ЧГ=0; ЧН=0")); + КонецЕсли; + + Возврат Результат; + +КонецФункции + +Функция ПортПоУмолчанию(Знач Схема, Знач Порт) + Возврат (Схема = "http" И Порт = 80) ИЛИ (Схема = "https" И Порт = 443); +КонецФункции + +Функция ЭтоЛокальныйХост(Знач Хост) + + Ниж = НРег(Хост); + + Возврат Ниж = "localhost" + ИЛИ Ниж = "127.0.0.1" + ИЛИ Ниж = "::1" + ИЛИ Ниж = "[::1]" + ИЛИ СтрЗаканчиваетсяНа(Ниж, ".localhost") + ИЛИ СтрНачинаетсяС(Ниж, "127."); + +КонецФункции + +Лог = Логирование.ПолучитьЛог(КонстантыOpm.ИмяЛога); diff --git "a/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\232\320\276\320\275\321\201\321\202\320\260\320\275\321\202\321\213Opm.os" "b/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\232\320\276\320\275\321\201\321\202\320\260\320\275\321\202\321\213Opm.os" index 0a6bfbd..40399fc 100644 --- "a/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\232\320\276\320\275\321\201\321\202\320\260\320\275\321\202\321\213Opm.os" +++ "b/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\232\320\276\320\275\321\201\321\202\320\260\320\275\321\202\321\213Opm.os" @@ -24,5 +24,5 @@ ПутьВЗапасномХранилище = "/download/"; ЛокальныйКаталогУстановкиПакетов = "oscript_modules"; ИмяЛога = "oscript.app.opm"; -ВерсияПродукта = "1.6.5"; +ВерсияПродукта = "1.7.0"; ИмяФайлаНастроек = "opm.cfg"; diff --git "a/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\232\320\276\320\275\321\202\321\200\320\276\320\273\321\214\320\275\321\213\320\265\320\241\321\203\320\274\320\274\321\213.os" "b/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\232\320\276\320\275\321\202\321\200\320\276\320\273\321\214\320\275\321\213\320\265\320\241\321\203\320\274\320\274\321\213.os" new file mode 100644 index 0000000..2dc3e6f --- /dev/null +++ "b/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\232\320\276\320\275\321\202\321\200\320\276\320\273\321\214\320\275\321\213\320\265\320\241\321\203\320\274\320\274\321\213.os" @@ -0,0 +1,193 @@ +/////////////////////////////////////////////////////////////////// +// +// Сверка скачанного файла пакета с контрольной суммой хаба +// (заголовки X-Checksum-Sha256 и ETag). +// +////////////////////////////////////////////////////////////////// + +#Использовать logos + +Перем Лог; + +// Режим, при котором проверка не выполняется вовсе. +// +// Возвращаемое значение: +// Строка. +// +Функция РежимВыключено() Экспорт + Возврат "выкл"; +КонецФункции + +// Режим по умолчанию: заголовок есть — сверяем, нет — ставим как раньше. +// +// Возвращаемое значение: +// Строка. +// +Функция РежимМягкий() Экспорт + Возврат "мягкая"; +КонецФункции + +// Режим, в котором отсутствие контрольной суммы у хаба — ошибка установки. +// +// Возвращаемое значение: +// Строка. +// +Функция РежимСтрогий() Экспорт + Возврат "строгая"; +КонецФункции + +// Приводит значение настройки к одному из известных режимов. +// +// Параметры: +// Значение - Строка, Булево - значение из настроек, опции или переменной среды. +// +// Возвращаемое значение: +// Строка - выкл, мягкая либо строгая. +// +Функция НормализоватьРежим(Знач Значение) Экспорт + + Если ТипЗнч(Значение) = Тип("Булево") Тогда + Возврат ?(Значение, РежимСтрогий(), РежимВыключено()); + КонецЕсли; + + Ниж = НРег(СокрЛП(Строка(Значение))); + + Если Ниж = "выкл" ИЛИ Ниж = "off" ИЛИ Ниж = "нет" ИЛИ Ниж = "no" ИЛИ Ниж = "false" ИЛИ Ниж = "0" Тогда + Возврат РежимВыключено(); + КонецЕсли; + + Если Ниж = "строгая" ИЛИ Ниж = "strict" ИЛИ Ниж = "да" ИЛИ Ниж = "yes" ИЛИ Ниж = "true" ИЛИ Ниж = "1" Тогда + Возврат РежимСтрогий(); + КонецЕсли; + + Возврат РежимМягкий(); + +КонецФункции + +// Контрольная сумма, объявленная хабом в заголовках ответа. +// X-Checksum-Sha256 приоритетнее; ETag читается только как sha256 в кавычках, +// потому что на чужом сервере это может быть любой непрозрачный маркер. +// +// Параметры: +// Заголовки - Соответствие, Неопределено - заголовки ответа. +// +// Возвращаемое значение: +// Строка - sha256 в нижнем регистре либо пустая строка, если хаб её не объявил. +// +Функция ОбъявленнаяСумма(Знач Заголовки) Экспорт + + Если Заголовки = Неопределено Тогда + Возврат ""; + КонецЕсли; + + Значение = ЗначениеЗаголовка(Заголовки, "X-Checksum-Sha256"); + Если ЭтоSHA256(Значение) Тогда + Возврат НРег(Значение); + КонецЕсли; + + ETag = СтрЗаменить(ЗначениеЗаголовка(Заголовки, "ETag"), """", ""); + Если СтрНачинаетсяС(НРег(ETag), "w/") Тогда + ETag = Сред(ETag, 3); + КонецЕсли; + Если ЭтоSHA256(ETag) Тогда + Возврат НРег(ETag); + КонецЕсли; + + Возврат ""; + +КонецФункции + +// SHA-256 файла в виде hex-строки нижнего регистра. +// +// Параметры: +// ПутьКФайлу - Строка - полный путь к файлу. +// +// Возвращаемое значение: +// Строка - 64 hex-символа в нижнем регистре. +// +Функция СуммаФайла(Знач ПутьКФайлу) Экспорт + + Хеширование = Новый ХешированиеДанных(ХешФункция.SHA256); + Хеширование.ДобавитьФайл(ПутьКФайлу); + + Возврат НРег(ПолучитьHexСтрокуИзДвоичныхДанных(Хеширование.ХешСумма)); + +КонецФункции + +// Сверяет скачанный файл с контрольной суммой хаба. +// Старый хаб заголовка не присылает: в режимах «выкл» и «мягкая» это не ошибка. +// +// Параметры: +// ПутьКФайлу - Строка - полный путь к скачанному файлу. +// Заголовки - Соответствие, Неопределено - заголовки ответа хаба. +// ИмяРесурса - Строка - имя ресурса для текста ошибки. +// Режим - Строка - выкл, мягкая либо строгая. +// +// Выбрасывает исключение при расхождении сумм, а в строгом режиме — +// и при отсутствии контрольной суммы у хаба. +// +Процедура ПроверитьЗагрузку(Знач ПутьКФайлу, Знач Заголовки, Знач ИмяРесурса, Знач Режим) Экспорт + + ТекущийРежим = НормализоватьРежим(Режим); + + Если ТекущийРежим = РежимВыключено() Тогда + Возврат; + КонецЕсли; + + Ожидаемая = ОбъявленнаяСумма(Заголовки); + + Если ПустаяСтрока(Ожидаемая) Тогда + Если ТекущийРежим = РежимСтрогий() Тогда + ВызватьИсключение СтрШаблон( + "Ошибка проверки %1: хаб не сообщил контрольную сумму (X-Checksum-Sha256), " + + "а включён строгий режим проверки", ИмяРесурса); + КонецЕсли; + Лог.Отладка("Хаб не сообщил контрольную сумму для %1 — проверка пропущена", ИмяРесурса); + Возврат; + КонецЕсли; + + Фактическая = СуммаФайла(ПутьКФайлу); + + Если Фактическая <> Ожидаемая Тогда + ВызватьИсключение СтрШаблон( + "Ошибка проверки %1: контрольная сумма не совпала. Ожидалась %2, получена %3", + ИмяРесурса, Ожидаемая, Фактическая); + КонецЕсли; + + Лог.Отладка("Контрольная сумма %1 совпала (%2)", ИмяРесурса, Ожидаемая); + +КонецПроцедуры + +/////////////////////////////////////////////////////////////////// + +Функция ЗначениеЗаголовка(Знач Заголовки, Знач Имя) + + Искомое = ВРег(Имя); + + Для Каждого КлючИЗначение Из Заголовки Цикл + Если ВРег(Строка(КлючИЗначение.Ключ)) = Искомое Тогда + Возврат СокрЛП(Строка(КлючИЗначение.Значение)); + КонецЕсли; + КонецЦикла; + + Возврат ""; + +КонецФункции + +Функция ЭтоSHA256(Знач Значение) + + Если СтрДлина(Значение) <> 64 Тогда + Возврат Ложь; + КонецЕсли; + + Для Индекс = 1 По 64 Цикл + Если СтрНайти("0123456789abcdefABCDEF", Сред(Значение, Индекс, 1)) = 0 Тогда + Возврат Ложь; + КонецЕсли; + КонецЦикла; + + Возврат Истина; + +КонецФункции + +Лог = Логирование.ПолучитьЛог(КонстантыOpm.ИмяЛога); diff --git "a/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\235\320\260\321\201\321\202\321\200\320\276\320\271\320\272\320\270Opm.os" "b/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\235\320\260\321\201\321\202\321\200\320\276\320\271\320\272\320\270Opm.os" index da71e7c..b63d8c8 100644 --- "a/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\235\320\260\321\201\321\202\321\200\320\276\320\271\320\272\320\270Opm.os" +++ "b/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\235\320\260\321\201\321\202\321\200\320\276\320\271\320\272\320\270Opm.os" @@ -89,6 +89,84 @@ мНастройки.СервераПакетов.Добавить(СерверПакетов); КонецПроцедуры +// Устанавливает режим сверки скачанного файла с контрольной суммой хаба. +// +// Параметры: +// ЗначениеНастройки - Строка, Булево - выкл, мягкая либо строгая. +// +Процедура УстановитьПроверкуКонтрольнойСуммы(Знач ЗначениеНастройки) Экспорт + + мНастройки.ПроверкаКонтрольнойСуммы = КонтрольныеСуммы.НормализоватьРежим(ЗначениеНастройки); + +КонецПроцедуры + +// Значение заголовка Authorization, описанное в настройках для той же схемы, хоста и порта. +// Позволяет ходить по адресу из командной строки под токеном, записанным opm login. +// +// Схема — часть ключа наравне с хостом и портом: без неё вход, сохранённый для +// https://хаб:443, подошёл бы и адресу http://хаб:443, и токен уехал бы на тот же хаб +// по незашифрованному каналу. +// +// Параметры: +// РазобранныйАдрес - Структура - результат АдресаХаба.Разобрать(). +// +// Возвращаемое значение: +// Строка - значение заголовка Authorization либо пустая строка. +// +Функция АвторизацияДляАдреса(Знач РазобранныйАдрес) Экспорт + + ИскомаяСхема = НРег(Строка(РазобранныйАдрес.Схема)); + ИскомыйХост = НРег(РазобранныйАдрес.Хост); + ИскомыйПорт = АдресаХаба.ЭффективныйПорт(РазобранныйАдрес.Схема, РазобранныйАдрес.Порт); + ПутьПула = НРег(АдресаХаба.ПутьВыдачи(РазобранныйАдрес.Пул)); + + Запасной = ""; + + Для Каждого НастройкаСервера Из ПолучитьНастройки().СервераПакетов Цикл + + Если НЕ ЗначениеЗаполнено(НастройкаСервера.Авторизация) Тогда + Продолжить; + КонецЕсли; + + Координаты = КоординатыСервера(НастройкаСервера); + Если Координаты = Неопределено + ИЛИ Координаты.Схема <> ИскомаяСхема + ИЛИ Координаты.Хост <> ИскомыйХост + ИЛИ Координаты.Порт <> ИскомыйПорт Тогда + Продолжить; + КонецЕсли; + + Если НРег(Строка(НастройкаСервера.ПутьНаСервере)) = ПутьПула Тогда + Возврат Строка(НастройкаСервера.Авторизация); + КонецЕсли; + + Если ПустаяСтрока(Запасной) Тогда + Запасной = Строка(НастройкаСервера.Авторизация); + КонецЕсли; + + КонецЦикла; + + Возврат Запасной; + +КонецФункции + +// Схема, хост и порт описанного сервера. Порт может быть и в поле Порт, и внутри адреса: +// opm push поле Порт не читает, поэтому в конфигурациях его дублируют в адресе. +Функция КоординатыСервера(Знач НастройкаСервера) + + Попытка + Разобранный = АдресаХаба.Разобрать(Строка(НастройкаСервера.Сервер)); + Исключение + Возврат Неопределено; + КонецПопытки; + + Порт = ?(НастройкаСервера.Порт = 0, Разобранный.Порт, НастройкаСервера.Порт); + + Возврат Новый Структура("Схема, Хост, Порт", НРег(Разобранный.Схема), НРег(Разобранный.Хост), + АдресаХаба.ЭффективныйПорт(Разобранный.Схема, Порт)); + +КонецФункции + Процедура СброситьНастройки() Экспорт @@ -111,7 +189,8 @@ мНастройки.Вставить("НастройкиПрокси", Новый Структура("Сервер, Порт, Пользователь, Пароль, ИспользоватьАутентификациюОС", "","","","", Ложь)); мНастройки.Вставить("СоздаватьShСкриптЗапуска", Ложь); мНастройки.Вставить("СервераПакетов", Новый Массив); - + мНастройки.Вставить("ПроверкаКонтрольнойСуммы", КонтрольныеСуммы.РежимМягкий()); + КонецПроцедуры Лог = Логирование.ПолучитьЛог("oscript.app.opm"); diff --git "a/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\236\320\261\321\211\320\265\320\263\320\276\320\235\320\260\320\267\320\275\320\260\321\207\320\265\320\275\320\270\321\217Opm.os" "b/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\236\320\261\321\211\320\265\320\263\320\276\320\235\320\260\320\267\320\275\320\260\321\207\320\265\320\275\320\270\321\217Opm.os" index 8dc5802..c5823dc 100644 --- "a/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\236\320\261\321\211\320\265\320\263\320\276\320\235\320\260\320\267\320\275\320\260\321\207\320\265\320\275\320\270\321\217Opm.os" +++ "b/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\236\320\261\321\211\320\265\320\263\320\276\320\235\320\260\320\267\320\275\320\260\321\207\320\265\320\275\320\270\321\217Opm.os" @@ -12,6 +12,28 @@ Возврат ПрименитьПеременныеСредыШаг(Значение, ПеременныеСреды, 0); КонецФункции +// Разбирает строку JSON в объект. +// +// Параметры: +// Текст - Строка - разбираемая строка. +// +// Возвращаемое значение: +// Соответствие - разобранный объект; Неопределено, если текст не JSON-объект +Функция РазобратьJSONОбъект(Знач Текст) Экспорт + + Попытка + Чтение = Новый ЧтениеJSON(); + Чтение.УстановитьСтроку(Текст); + Результат = ПрочитатьJSON(Чтение, Истина); + Чтение.Закрыть(); + Исключение + Возврат Неопределено; + КонецПопытки; + + Возврат ?(ТипЗнч(Результат) = Тип("Соответствие"), Результат, Неопределено); + +КонецФункции + Процедура СообщениеПользователю(Знач Лог, Знач СтрокаСообщения, Знач ПараметрШаблона1 = Неопределено) Экспорт Сообщить(СтрШаблон(СтрокаСообщения, ПараметрШаблона1)); Лог.Отладка(СтрокаСообщения, ПараметрШаблона1); diff --git "a/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\236\321\210\320\270\320\261\320\272\320\270\320\241\320\265\321\202\320\270.os" "b/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\236\321\210\320\270\320\261\320\272\320\270\320\241\320\265\321\202\320\270.os" new file mode 100644 index 0000000..03731d5 --- /dev/null +++ "b/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\236\321\210\320\270\320\261\320\272\320\270\320\241\320\265\321\202\320\270.os" @@ -0,0 +1,114 @@ +/////////////////////////////////////////////////////////////////// +// +// Перевод транспортных исключений платформы в человеческую причину: +// имя не разрешается, соединение отклонено, таймаут, TLS. +// +////////////////////////////////////////////////////////////////// + +// Причина неудачного обращения к хабу одной фразой, без служебной обёртки исключения. +// +// Параметры: +// ТекстОшибки - Строка - ОписаниеОшибки() платформы. +// +// Возвращаемое значение: +// Строка - причина в терминах пользователя. +// +Функция Причина(Знач ТекстОшибки) Экспорт + + Суть = Суть(ТекстОшибки); + Ниж = НРег(Суть); + + Если Содержит(Ниж, "nodename nor servname") + ИЛИ Содержит(Ниж, "no such host") + ИЛИ Содержит(Ниж, "name or service not known") + ИЛИ Содержит(Ниж, "getaddrinfo") + ИЛИ Содержит(Ниж, "host not found") + ИЛИ Содержит(Ниж, "nameresolution") Тогда + Возврат "имя хоста не разрешается"; + КонецЕсли; + + Если Содержит(Ниж, "connection refused") ИЛИ Содержит(Ниж, "actively refused") Тогда + Возврат "соединение отклонено — по этому адресу никто не слушает"; + КонецЕсли; + + Если Содержит(Ниж, "timed out") ИЛИ Содержит(Ниж, "timeout") Тогда + Возврат "истекло время ожидания ответа"; + КонецЕсли; + + Если Содержит(Ниж, "ssl") ИЛИ Содержит(Ниж, "tls") + ИЛИ Содержит(Ниж, "certificate") ИЛИ Содержит(Ниж, "secure channel") Тогда + Возврат "не удалось установить защищённое соединение (TLS)"; + КонецЕсли; + + Если Содержит(Ниж, "network is unreachable") ИЛИ Содержит(Ниж, "unreachable") Тогда + Возврат "сеть недоступна"; + КонецЕсли; + + Если Содержит(Ниж, "forcibly closed") ИЛИ Содержит(Ниж, "connection reset") Тогда + Возврат "соединение разорвано другой стороной"; + КонецЕсли; + + Возврат ?(ПустаяСтрока(Суть), "соединение не установлено", Суть); + +КонецФункции + +// Готовое сообщение об отказе для явно названного адреса хаба. +// +// Параметры: +// БазовыйАдрес - Строка - адрес хаба, как его назвал пользователь. +// ТекстОшибки - Строка - ОписаниеОшибки() платформы. +// +// Возвращаемое значение: +// Строка - строка вида "Не удалось подключиться к <адрес>: <причина>". +// +Функция НеУдалосьПодключиться(Знач БазовыйАдрес, Знач ТекстОшибки) Экспорт + + Возврат СтрШаблон("Не удалось подключиться к %1: %2", БазовыйАдрес, Причина(ТекстОшибки)); + +КонецФункции + +// Снимает служебную обёртку платформы: имя модуля, номер строки и тип .NET-исключения. +// В пользовательский текст они не идут, их место — в отладочном выводе. +// +// Параметры: +// ТекстОшибки - Строка - ОписаниеОшибки() платформы. +// +// Возвращаемое значение: +// Строка - суть сообщения. +// +Функция Суть(Знач ТекстОшибки) Экспорт + + Исходный = СокрЛП(Строка(ТекстОшибки)); + + // Обёртку узнаём по обеим приметам сразу: тело ответа хаба тоже начинается с фигурной + // скобки, и трогать его переносы строк нельзя. + Если НЕ СтрНачинаетсяС(Исходный, "{Модуль ") ИЛИ СтрНайти(Исходный, " / Error in line") = 0 Тогда + Возврат Исходный; + КонецЕсли; + + Значение = СокрЛП(СтрЗаменить(СтрЗаменить(Исходный, Символы.ПС, " "), Символы.ВК, "")); + Если СтрЗаканчиваетсяНа(Значение, "}") Тогда + Значение = СокрЛП(Лев(Значение, СтрДлина(Значение) - 1)); + КонецЕсли; + + // "{Модуль …/Файл.os / Error in line: 69 / текст}" — берём текст после последнего " / " + Позиция = СтрНайти(Значение, " / ", НаправлениеПоиска.СКонца); + Если Позиция > 0 Тогда + Значение = СокрЛП(Сред(Значение, Позиция + 3)); + КонецЕсли; + + // "Внешнее исключение (System.Net.WebException): текст" — тип исключения не нужен + Позиция = СтрНайти(Значение, "):"); + Если СтрНачинаетсяС(Значение, "Внешнее исключение") И Позиция > 0 Тогда + Значение = СокрЛП(Сред(Значение, Позиция + 2)); + КонецЕсли; + + Возврат Значение; + +КонецФункции + +/////////////////////////////////////////////////////////////////// + +Функция Содержит(Знач Строка_, Знач Подстрока) + Возврат СтрНайти(Строка_, Подстрока) > 0; +КонецФункции diff --git "a/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\240\320\260\320\261\320\276\321\202\320\260\320\241\320\222\320\265\321\200\321\201\320\270\321\217\320\274\320\270.os" "b/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\240\320\260\320\261\320\276\321\202\320\260\320\241\320\222\320\265\321\200\321\201\320\270\321\217\320\274\320\270.os" index 61e5c9e..7695e09 100644 --- "a/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\240\320\260\320\261\320\276\321\202\320\260\320\241\320\222\320\265\321\200\321\201\320\270\321\217\320\274\320\270.os" +++ "b/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\240\320\260\320\261\320\276\321\202\320\260\320\241\320\222\320\265\321\200\321\201\320\270\321\217\320\274\320\270.os" @@ -147,6 +147,66 @@ КонецФункции +// Может ли строка быть номером опубликованной версии, то есть именем артефакта +// на хабе, который ничего не разрешает. Номер — цифры, разделённые точками, с +// необязательным суффиксом после дефиса или плюса; плюс особая версия SNAPSHOT. +// +// Параметры: +// Значение - Строка - строка после символа @. +// +// Возвращаемое значение: +// Булево - Истина, если строка может оказаться номером версии. +// +Функция ЭтоТочныйНомерВерсии(Знач Значение) Экспорт + + Строка_ = СокрЛП(Строка(Значение)); + + Если ПустаяСтрока(Строка_) Тогда + Возврат Ложь; + КонецЕсли; + + Верхняя = ВРег(Строка_); + Если Верхняя = "SNAPSHOT" ИЛИ СтрЗаканчиваетсяНа(Верхняя, "-SNAPSHOT") Тогда + Возврат Истина; + КонецЕсли; + + Возврат ЭтоЦепочкаЧисел(ЧастьДоСуффикса(Строка_)); + +КонецФункции + +// Суффикс версии начинается с дефиса (пререлиз) либо плюса (сборка). +Функция ЧастьДоСуффикса(Знач Значение) + + Возврат СтрРазделить(СтрРазделить(Значение, "-")[0], "+")[0]; + +КонецФункции + +Функция ЭтоЦепочкаЧисел(Знач Значение) + + Составляющие = СтрРазделить(Значение, ".", Истина); + + Если Составляющие.Количество() = 0 Тогда + Возврат Ложь; + КонецЕсли; + + Для Каждого Составляющая Из Составляющие Цикл + + Если ПустаяСтрока(Составляющая) Тогда + Возврат Ложь; + КонецЕсли; + + Для Индекс = 1 По СтрДлина(Составляющая) Цикл + Если СтрНайти("0123456789", Сред(Составляющая, Индекс, 1)) = 0 Тогда + Возврат Ложь; + КонецЕсли; + КонецЦикла; + + КонецЦикла; + + Возврат Истина; + +КонецФункции + Функция РазобратьИмяПакета(знач ИмяПакета) Экспорт Пакет = Новый Структура("ИмяПакета,Версия","",Неопределено); diff --git "a/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\240\320\260\320\261\320\276\321\202\320\260\320\241\320\237\320\260\320\272\320\265\321\202\320\260\320\274\320\270.os" "b/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\240\320\260\320\261\320\276\321\202\320\260\320\241\320\237\320\260\320\272\320\265\321\202\320\260\320\274\320\270.os" index bf36600..54342ae 100644 --- "a/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\240\320\260\320\261\320\276\321\202\320\260\320\241\320\237\320\260\320\272\320\265\321\202\320\260\320\274\320\270.os" +++ "b/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\240\320\260\320\261\320\276\321\202\320\260\320\241\320\237\320\260\320\272\320\265\321\202\320\260\320\274\320\270.os" @@ -108,6 +108,54 @@ КонецПроцедуры +// Устанавливает пакет по адресу хаба, указанному прямо в командной строке: +// [схема://]хост[:порт]/[пул/]пакет[@спецификация]. Запись в opm.cfg не требуется; +// токен, если он там описан для того же хоста, подхватывается. Адрес без пула ведёт +// в пул хаба по умолчанию. +// +// Параметры: +// Адрес - Строка - адрес пакета в хабе. +// РежимУстановки - РежимУстановкиПакетов - режим установки. +// ЦелевойКаталог - Строка, Неопределено - каталог установки. +// НастройкаУстановки - Структура, Неопределено - настройка установки. +// +Процедура УстановитьПакетПоАдресу(Знач Адрес, Знач РежимУстановки, Знач ЦелевойКаталог = Неопределено, + Знач НастройкаУстановки = Неопределено) Экспорт + + РазобранныйАдрес = АдресаХаба.РазобратьАдресПакета(Адрес); + + Авторизация = НастройкиOpm.АвторизацияДляАдреса(РазобранныйАдрес); + Если ЗначениеЗаполнено(Авторизация) Тогда + АдресаХаба.ПредупредитьОбОткрытомКанале(РазобранныйАдрес); + КонецЕсли; + + Сервер = СерверыПакетов.ИзАдреса(РазобранныйАдрес, Авторизация); + НастройкиOpm.ДобавитьСерверПакетов(Сервер); + СброситьМенеджерПолученияПакетов(); + + Если НастройкаУстановки = Неопределено Тогда + НастройкаУстановки = ПолучитьНастройкуУстановки(); + КонецЕсли; + + ЭтоЗависимость = (РежимУстановки = РежимУстановкиПакетов.Локально); + + МенеджерУстановки = Новый МенеджерУстановкиПакетов(РежимУстановки, ЦелевойКаталог, , Сервер.ПолучитьИмя()); + МенеджерУстановки.ЗакрепитьСерверЗаКорневымПакетом(Истина); + МенеджерУстановки.УстанавливатьЗависимости(НастройкаУстановки.УстанавливатьЗависимости); + МенеджерУстановки.СоздаватьФайлыЗапуска(НастройкаУстановки.СоздаватьФайлыЗапуска); + МенеджерУстановки.УстанавливатьЗависимостиРазработчика(НастройкаУстановки.УстанавливатьЗависимостиРазработчика); + + // пустая спецификация не схлопывается в «не названа»: «пакет@» — отказ, а не выбор хаба + МенеджерУстановки.УстановитьПакетПоИмениИВерсии(РазобранныйАдрес.ИмяПакета, + РазобранныйАдрес.Спецификация, ЭтоЗависимость); + +КонецПроцедуры + +// Сбрасывает кеш менеджера получения: после добавления сервера индекс пакетов устарел. +Процедура СброситьМенеджерПолученияПакетов() Экспорт + ВнутреннийМенеджерПолученияПакетов = Неопределено; +КонецПроцедуры + Процедура УстановитьПакетИзФайла(Знач ИмяФайлаПакета, Знач РежимУстановки, Знач ЦелевойКаталог = Неопределено, @@ -226,10 +274,23 @@ КонецПроцедуры -Функция ПолучитьПакет(Знач ИмяПакета, Знач ВерсияПакета, ПутьКФайлуПакета = "", ИмяСервера = "") Экспорт - +// Скачивает файл пакета из хаба. Единственная воронка загрузки: через неё идут и установка, +// и обновление, и скачивание, и разрешение зависимостей. +// +// Параметры: +// ИмяПакета - Строка - имя пакета в хабе. +// Спецификация - Строка, Неопределено - то, что пользователь написал после @: точный номер +// версии, имя тега либо выражение версии. +// ПутьКФайлуПакета - Строка - куда положить скачанный файл; пустое даёт временный файл. +// ИмяСервера - Строка - имя сервера пакетов; пустое означает перебор всех известных. +// +// Возвращаемое значение: +// Строка - путь к скачанному файлу пакета. +// +Функция ПолучитьПакет(Знач ИмяПакета, Знач Спецификация, ПутьКФайлуПакета = "", ИмяСервера = "") Экспорт + МенеджерПолучения = ПолучитьМенеджерПолученияПакетов(); - ПутьКФайлуПакета = МенеджерПолучения.ПолучитьПакет(ИмяПакета, ВерсияПакета, ПутьКФайлуПакета, ИмяСервера); + ПутьКФайлуПакета = МенеджерПолучения.ПолучитьПакет(ИмяПакета, Спецификация, ПутьКФайлуПакета, ИмяСервера); Возврат ПутьКФайлуПакета; diff --git "a/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\241\320\265\321\200\320\262\320\265\321\200\321\213\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" "b/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\241\320\265\321\200\320\262\320\265\321\200\321\213\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" index d44abf1..009d6f5 100644 --- "a/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\241\320\265\321\200\320\262\320\265\321\200\321\213\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" +++ "b/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\241\320\265\321\200\320\262\320\265\321\200\321\213\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" @@ -27,6 +27,34 @@ КонецФункции +// Собирает сервер пакетов по адресу, указанному прямо в командной строке. +// +// Параметры: +// РазобранныйАдрес - Структура - результат АдресаХаба.Разобрать(). +// Авторизация - Строка - значение заголовка Authorization; пустое — без авторизации. +// +// Возвращаемое значение: +// СерверПакетов - сервер, готовый и к выдаче, и к публикации. +// +Функция ИзАдреса(Знач РазобранныйАдрес, Знач Авторизация = "") Экспорт + + Сервер = Новый СерверПакетов( + АдресаХаба.ИмяСервераДляАдреса(РазобранныйАдрес), + СтрШаблон("%1://%2", РазобранныйАдрес.Схема, РазобранныйАдрес.Хост), + АдресаХаба.ПутьВыдачи(РазобранныйАдрес.Пул), + АдресаХаба.ПутьПубликации(РазобранныйАдрес.Пул), + АдресаХаба.ЭффективныйПорт(РазобранныйАдрес.Схема, РазобранныйАдрес.Порт), + 0, + Авторизация, + Новый Соответствие(), + 600); + + Сервер.НазванЯвно = Истина; + + Возврат Сервер; + +КонецФункции + Функция ОсновнойСервер() Экспорт Возврат Новый СерверПакетов("ОсновнойСерверПакетов", КонстантыOpm.СерверУдаленногоХранилища, diff --git "a/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\244\320\260\320\271\320\273\321\213\320\235\320\260\321\201\321\202\321\200\320\276\320\265\320\272Opm.os" "b/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\244\320\260\320\271\320\273\321\213\320\235\320\260\321\201\321\202\321\200\320\276\320\265\320\272Opm.os" new file mode 100644 index 0000000..e9a632b --- /dev/null +++ "b/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\244\320\260\320\271\320\273\321\213\320\235\320\260\321\201\321\202\321\200\320\276\320\265\320\272Opm.os" @@ -0,0 +1,346 @@ +/////////////////////////////////////////////////////////////////// +// +// Файлы настроек opm: где они лежат, какой из них действует +// и как записать в домашний конфиг сервер пакетов с токеном. +// +////////////////////////////////////////////////////////////////// + +#Использовать 1commands +#Использовать logos + +Перем Лог; + +// Пути, в которых opm ищет файл настроек, в порядке поиска. +// Побеждает первый найденный, слияния между файлами нет. +// +// Возвращаемое значение: +// Массив из Строка - текущий каталог, профиль пользователя, системный, каталог сценария. +// +Функция СписокФайловНастроек() Экспорт + + ИмяФайла = КонстантыOpm.ИмяФайлаНастроек; + МассивФайлов = Новый Массив; + + МассивФайлов.Добавить(ОбъединитьПути(ТекущийКаталог(), ИмяФайла)); + МассивФайлов.Добавить(ПутьДомашнегоФайла()); + + Если ЭтоWindows() Тогда + МассивФайлов.Добавить(ОбъединитьПути(ПолучитьПеременнуюСреды(ВРег("ALLUSERSPROFILE")), ИмяФайла)); + Иначе + МассивФайлов.Добавить(ОбъединитьПути("/etc", ИмяФайла)); + КонецЕсли; + + МассивФайлов.Добавить(ОбъединитьПути(СтартовыйСценарий().Каталог, ИмяФайла)); + + Возврат МассивФайлов; + +КонецФункции + +// Путь к файлу настроек в профиле пользователя. +// +// Возвращаемое значение: +// Строка - полный путь к ~/.opm.cfg либо %USERPROFILE%\opm.cfg. +// +Функция ПутьДомашнегоФайла() Экспорт + + ИмяФайла = КонстантыOpm.ИмяФайлаНастроек; + + Если ЭтоWindows() Тогда + Возврат ОбъединитьПути(ПолучитьПеременнуюСреды(ВРег("USERPROFILE")), ИмяФайла); + КонецЕсли; + + Возврат ОбъединитьПути(ПолучитьПеременнуюСреды(ВРег("HOME")), "." + ИмяФайла); + +КонецФункции + +// Файл настроек, который реально победит при запуске opm. +// +// Возвращаемое значение: +// Строка - путь к первому существующему файлу настроек либо пустая строка. +// +Функция ПутьДействующегоФайла() Экспорт + + Для Каждого Путь Из СписокФайловНастроек() Цикл + Если Новый Файл(Путь).Существует() Тогда + Возврат Путь; + КонецЕсли; + КонецЦикла; + + Возврат ""; + +КонецФункции + +// Читает файл настроек в структуру. Отсутствующий файл даёт пустую структуру. +// +// Параметры: +// ПутьКФайлу - Строка - путь к файлу настроек. +// +// Возвращаемое значение: +// Структура - содержимое файла настроек. +// +Функция ПрочитатьНастройки(Знач ПутьКФайлу) Экспорт + + Если НЕ Новый Файл(ПутьКФайлу).Существует() Тогда + Возврат Новый Структура(); + КонецЕсли; + + Чтение = Новый ЧтениеТекста(ПутьКФайлу); + Текст = Чтение.Прочитать(); + Чтение.Закрыть(); + + Если НЕ ЗначениеЗаполнено(Текст) Тогда + Возврат Новый Структура(); + КонецЕсли; + + ЧтениеJSON = Новый ЧтениеJSON(); + ЧтениеJSON.УстановитьСтроку(Текст); + Прочитанное = ПрочитатьJSON(ЧтениеJSON, Истина); + ЧтениеJSON.Закрыть(); + + Результат = Новый Структура(); + Для Каждого КлючИЗначение Из Прочитанное Цикл + Результат.Вставить(КлючИЗначение.Ключ, КлючИЗначение.Значение); + КонецЦикла; + + Возврат Результат; + +КонецФункции + +// Записывает настройки в файл, доступный только владельцу: в файле лежит токен, +// поэтому права закрываются до того, как в файл попадёт содержимое. +// +// Параметры: +// Настройки - Структура - записываемые настройки. +// ПутьКФайлу - Строка - путь к файлу настроек. +// +Процедура ЗаписатьНастройки(Знач Настройки, Знач ПутьКФайлу) Экспорт + + ПараметрыЗаписи = Новый ПараметрыЗаписиJSON(ПереносСтрокJSON.Unix, " "); + ЗаписьJSON = Новый ЗаписьJSON(); + ЗаписьJSON.УстановитьСтроку(ПараметрыЗаписи); + ЗаписатьJSON(ЗаписьJSON, Настройки); + Текст = ЗаписьJSON.Закрыть(); + + СоздатьПустойФайл(ПутьКФайлу); + ЗакрытьДоступПосторонним(ПутьКФайлу); + + Запись = Новый ЗаписьТекста(ПутьКФайлу); + Запись.ЗаписатьСтроку(Текст); + Запись.Закрыть(); + +КонецПроцедуры + +// Добавляет либо обновляет описание сервера пакетов с токеном авторизации. +// Сервер узнаётся по адресу, порту и пути пула, а не по имени: повторный вход по тому же +// адресу обновляет запись, а не плодит дубли. Порт входит в ключ, потому что поле Сервер +// собирается без него: без порта вход в localhost:4400 затирал бы токен localhost:3380. +// +// Параметры: +// Настройки - Структура - настройки, прочитанные из файла. +// РазобранныйАдрес - Структура - результат АдресаХаба.Разобрать(). +// Токен - Строка - персональный токен доступа. +// +// Возвращаемое значение: +// Строка - имя сервера, под которым запись сохранена. +// +Функция ЗаписатьСерверПакетов(Настройки, Знач РазобранныйАдрес, Знач Токен) Экспорт + + Сервер = СтрШаблон("%1://%2", РазобранныйАдрес.Схема, РазобранныйАдрес.Хост); + Порт = ЭффективныйПорт(РазобранныйАдрес); + ПутьНаСервере = АдресаХаба.ПутьВыдачи(РазобранныйАдрес.Пул); + ИмяСервера = ИмяСервераДляЗаписи(РазобранныйАдрес); + + Список = Новый Массив(); + Если Настройки.Свойство("СервераПакетов") И ТипЗнч(Настройки.СервераПакетов) = Тип("Массив") Тогда + Список = Настройки.СервераПакетов; + КонецЕсли; + + Запись = Новый Соответствие(); + Запись.Вставить("Имя", ИмяСервера); + Запись.Вставить("Сервер", Сервер); + Запись.Вставить("Порт", Порт); + Запись.Вставить("ПутьНаСервере", ПутьНаСервере); + Запись.Вставить("РесурсПубликацииПакетов", АдресаХаба.ПутьПубликации(РазобранныйАдрес.Пул)); + Запись.Вставить("Авторизация", "Bearer " + СокрЛП(Строка(Токен))); + + Обновлено = Ложь; + Для Индекс = 0 По Список.Количество() - 1 Цикл + Существующая = Список[Индекс]; + Если ЭтоТаЖеЗапись(Существующая, Сервер, Порт, ПутьНаСервере) Тогда + Запись.Вставить("Имя", ?(ПустаяСтрока(ЗначениеПоля(Существующая, "Имя")), + ИмяСервера, ЗначениеПоля(Существующая, "Имя"))); + ИмяСервера = Запись["Имя"]; + Список[Индекс] = Запись; + Обновлено = Истина; + Прервать; + КонецЕсли; + КонецЦикла; + + Если НЕ Обновлено Тогда + Список.Добавить(Запись); + КонецЕсли; + + Настройки.Вставить("СервераПакетов", Список); + + Возврат ИмяСервера; + +КонецФункции + +// Удаляет описание сервера пакетов по адресу. +// +// Параметры: +// Настройки - Структура - настройки, прочитанные из файла. +// РазобранныйАдрес - Структура - результат АдресаХаба.Разобрать(). +// +// Возвращаемое значение: +// Булево - Истина, если запись была найдена и удалена. +// +Функция УдалитьСерверПакетов(Настройки, Знач РазобранныйАдрес) Экспорт + + Если НЕ Настройки.Свойство("СервераПакетов") ИЛИ ТипЗнч(Настройки.СервераПакетов) <> Тип("Массив") Тогда + Возврат Ложь; + КонецЕсли; + + Сервер = СтрШаблон("%1://%2", РазобранныйАдрес.Схема, РазобранныйАдрес.Хост); + Порт = ЭффективныйПорт(РазобранныйАдрес); + ПутьНаСервере = АдресаХаба.ПутьВыдачи(РазобранныйАдрес.Пул); + + Оставшиеся = Новый Массив(); + Удалено = Ложь; + + Для Каждого Существующая Из Настройки.СервераПакетов Цикл + Если ЭтоТаЖеЗапись(Существующая, Сервер, Порт, ПутьНаСервере) Тогда + Удалено = Истина; + Продолжить; + КонецЕсли; + Оставшиеся.Добавить(Существующая); + КонецЦикла; + + Настройки.Вставить("СервераПакетов", Оставшиеся); + + Возврат Удалено; + +КонецФункции + +// Имя сервера пакетов, под которым запись входа сохраняется в конфигурации. +// +// Параметры: +// РазобранныйАдрес - Структура - результат АдресаХаба.Разобрать(). +// +// Возвращаемое значение: +// Строка - имя вида "хост-пул". +// +Функция ИмяСервераДляЗаписи(Знач РазобранныйАдрес) Экспорт + + Основа = РазобранныйАдрес.Хост; + Если РазобранныйАдрес.Порт <> 0 Тогда + Основа = СтрШаблон("%1-%2", Основа, Формат(РазобранныйАдрес.Порт, "ЧГ=0; ЧН=0")); + КонецЕсли; + + Если ПустаяСтрока(РазобранныйАдрес.Пул) Тогда + Возврат Основа; + КонецЕсли; + + Возврат СтрШаблон("%1-%2", Основа, РазобранныйАдрес.Пул); + +КонецФункции + +/////////////////////////////////////////////////////////////////// + +// Одна и та же ли это запись сервера. Ключ — адрес, порт и путь пула: имя записи +// человек волен поменять, а два хаба на одном хосте различаются только портом. +Функция ЭтоТаЖеЗапись(Знач Запись, Знач Сервер, Знач Порт, Знач ПутьНаСервере) + + Возврат ЗначениеПоля(Запись, "Сервер") = Сервер + И ЗначениеПоля(Запись, "ПутьНаСервере") = ПутьНаСервере + И ПортЗаписи(Запись) = Порт; + +КонецФункции + +// Порт записи в том же виде, в каком он считается для адреса: отсутствующее +// и нулевое поле означают порт по схеме адреса записи. +Функция ПортЗаписи(Знач Запись) + + Значение = ЗначениеПоля(Запись, "Порт"); + + Попытка + Порт = ?(ПустаяСтрока(Значение), 0, Число(Значение)); + Исключение + Возврат 0; + КонецПопытки; + + Если Порт <> 0 Тогда + Возврат Порт; + КонецЕсли; + + Попытка + Возврат ЭффективныйПорт(АдресаХаба.Разобрать(ЗначениеПоля(Запись, "Сервер"))); + Исключение + Возврат 0; + КонецПопытки; + +КонецФункции + +Функция ЭффективныйПорт(Знач РазобранныйАдрес) + + Возврат АдресаХаба.ЭффективныйПорт(РазобранныйАдрес.Схема, РазобранныйАдрес.Порт); + +КонецФункции + +Функция ЗначениеПоля(Знач Запись, Знач Имя) + + Попытка + Если ТипЗнч(Запись) = Тип("Структура") Тогда + Значение = Неопределено; + Возврат ?(Запись.Свойство(Имя, Значение) И Значение <> Неопределено, Строка(Значение), ""); + КонецЕсли; + Значение = Запись[Имя]; + Возврат ?(Значение = Неопределено, "", Строка(Значение)); + Исключение + Возврат ""; + КонецПопытки; + +КонецФункции + +// Заводит файл пустым, чтобы было чему менять права до записи токена. +Процедура СоздатьПустойФайл(Знач ПутьКФайлу) + + Если Новый Файл(ПутьКФайлу).Существует() Тогда + Возврат; + КонецЕсли; + + Запись = Новый ЗаписьТекста(ПутьКФайлу); + Запись.Закрыть(); + +КонецПроцедуры + +Процедура ЗакрытьДоступПосторонним(Знач ПутьКФайлу) + + Если ЭтоWindows() Тогда + Возврат; + КонецЕсли; + + Попытка + Команда = Новый Команда(); + Команда.УстановитьКоманду("chmod"); + Команда.ДобавитьПараметр("600"); + Команда.ДобавитьПараметр(ПутьКФайлу); + Команда.ПоказыватьВыводНемедленно(Ложь); + Если Команда.Исполнить() <> 0 Тогда + Лог.Предупреждение("Не удалось ограничить права на файл настроек %1 — в нём лежит токен", ПутьКФайлу); + КонецЕсли; + Исключение + Лог.Предупреждение("Не удалось ограничить права на файл настроек %1 — в нём лежит токен", ПутьКФайлу); + КонецПопытки; + +КонецПроцедуры + +Функция ЭтоWindows() + + СистемнаяИнформация = Новый СистемнаяИнформация(); + + Возврат Найти(НРег(СистемнаяИнформация.ВерсияОС), "windows") > 0; + +КонецФункции + +Лог = Логирование.ПолучитьЛог(КонстантыOpm.ИмяЛога); diff --git a/tests/checksum-test.os b/tests/checksum-test.os new file mode 100644 index 0000000..cc7044d --- /dev/null +++ b/tests/checksum-test.os @@ -0,0 +1,151 @@ +#Использовать asserts +#Использовать tempfiles +#Использовать "../src/core" + +Перем юТест; +Перем ФайлПробы; +Перем СуммаПробы; + +Функция ПолучитьСписокТестов(Знач Тестирование) Экспорт + + юТест = Тестирование; + + СписокТестов = Новый Массив; + + СписокТестов.Добавить("ТестДолжен_НормализоватьРежимПроверки"); + СписокТестов.Добавить("ТестДолжен_ВзятьСуммуИзЗаголовкаКонтрольнойСуммы"); + СписокТестов.Добавить("ТестДолжен_ВзятьСуммуИзETag"); + СписокТестов.Добавить("ТестДолжен_ИгнорироватьНепохожийНаSHA256ETag"); + СписокТестов.Добавить("ТестДолжен_ПосчитатьСуммуФайла"); + СписокТестов.Добавить("ТестДолжен_ПропуститьПроверкуБезЗаголовкаВМягкомРежиме"); + СписокТестов.Добавить("ТестДолжен_ОтказатьБезЗаголовкаВСтрогомРежиме"); + СписокТестов.Добавить("ТестДолжен_ОтказатьПриРасхожденииСуммы"); + СписокТестов.Добавить("ТестДолжен_ПропуститьРасхождениеПриВыключеннойПроверке"); + СписокТестов.Добавить("ТестДолжен_ПринятьСовпавшуюСумму"); + + Возврат СписокТестов; + +КонецФункции + +Процедура ПередЗапускомТеста() Экспорт + + ФайлПробы = ВременныеФайлы.НовоеИмяФайла("bin"); + Запись = Новый ЗаписьТекста(ФайлПробы); + Запись.Записать("проба контрольной суммы"); + Запись.Закрыть(); + + СуммаПробы = КонтрольныеСуммы.СуммаФайла(ФайлПробы); + +КонецПроцедуры + +Процедура ПослеЗапускаТеста() Экспорт + + ВременныеФайлы.Удалить(); + +КонецПроцедуры + +Процедура ТестДолжен_НормализоватьРежимПроверки() Экспорт + + Ожидаем.Что(КонтрольныеСуммы.НормализоватьРежим("выкл"), "выкл").Равно(КонтрольныеСуммы.РежимВыключено()); + Ожидаем.Что(КонтрольныеСуммы.НормализоватьРежим("off"), "off").Равно(КонтрольныеСуммы.РежимВыключено()); + Ожидаем.Что(КонтрольныеСуммы.НормализоватьРежим(Ложь), "Ложь").Равно(КонтрольныеСуммы.РежимВыключено()); + Ожидаем.Что(КонтрольныеСуммы.НормализоватьРежим("строгая"), "строгая").Равно(КонтрольныеСуммы.РежимСтрогий()); + Ожидаем.Что(КонтрольныеСуммы.НормализоватьРежим(Истина), "Истина").Равно(КонтрольныеСуммы.РежимСтрогий()); + Ожидаем.Что(КонтрольныеСуммы.НормализоватьРежим(""), "пустое значение").Равно(КонтрольныеСуммы.РежимМягкий()); + Ожидаем.Что(КонтрольныеСуммы.НормализоватьРежим("что-то ещё"), "неизвестное значение") + .Равно(КонтрольныеСуммы.РежимМягкий()); + +КонецПроцедуры + +Процедура ТестДолжен_ВзятьСуммуИзЗаголовкаКонтрольнойСуммы() Экспорт + + Заголовки = Новый Соответствие(); + Заголовки.Вставить("x-checksum-sha256", ВРег(СуммаПробы)); + + Ожидаем.Что(КонтрольныеСуммы.ОбъявленнаяСумма(Заголовки), "регистр заголовка и значения не важен") + .Равно(СуммаПробы); + +КонецПроцедуры + +Процедура ТестДолжен_ВзятьСуммуИзETag() Экспорт + + Заголовки = Новый Соответствие(); + Заголовки.Вставить("ETag", СтрШаблон("W/""%1""", СуммаПробы)); + + Ожидаем.Что(КонтрольныеСуммы.ОбъявленнаяСумма(Заголовки), "слабый ETag в кавычках").Равно(СуммаПробы); + +КонецПроцедуры + +Процедура ТестДолжен_ИгнорироватьНепохожийНаSHA256ETag() Экспорт + + Заголовки = Новый Соответствие(); + Заголовки.Вставить("ETag", """непрозрачный-маркер-чужого-сервера"""); + + Ожидаем.Что(КонтрольныеСуммы.ОбъявленнаяСумма(Заголовки), "чужой ETag не выдаём за сумму").Равно(""); + Ожидаем.Что(КонтрольныеСуммы.ОбъявленнаяСумма(Неопределено), "заголовков нет вовсе").Равно(""); + +КонецПроцедуры + +Процедура ТестДолжен_ПосчитатьСуммуФайла() Экспорт + + Ожидаем.Что(СтрДлина(СуммаПробы), "длина hex-строки").Равно(64); + Ожидаем.Что(СуммаПробы, "сумма в нижнем регистре").Равно(НРег(СуммаПробы)); + +КонецПроцедуры + +Процедура ТестДолжен_ПропуститьПроверкуБезЗаголовкаВМягкомРежиме() Экспорт + + КонтрольныеСуммы.ПроверитьЗагрузку(ФайлПробы, Новый Соответствие(), "проба.ospx", + КонтрольныеСуммы.РежимМягкий()); + +КонецПроцедуры + +Процедура ТестДолжен_ОтказатьБезЗаголовкаВСтрогомРежиме() Экспорт + + ОжидатьОтказ(Новый Соответствие(), КонтрольныеСуммы.РежимСтрогий(), "не сообщил контрольную сумму"); + +КонецПроцедуры + +Процедура ТестДолжен_ОтказатьПриРасхожденииСуммы() Экспорт + + Заголовки = Новый Соответствие(); + Заголовки.Вставить("X-Checksum-Sha256", ШаблоннаяСумма("0")); + + ОжидатьОтказ(Заголовки, КонтрольныеСуммы.РежимМягкий(), "не совпала"); + +КонецПроцедуры + +Процедура ТестДолжен_ПропуститьРасхождениеПриВыключеннойПроверке() Экспорт + + Заголовки = Новый Соответствие(); + Заголовки.Вставить("X-Checksum-Sha256", ШаблоннаяСумма("0")); + + КонтрольныеСуммы.ПроверитьЗагрузку(ФайлПробы, Заголовки, "проба.ospx", КонтрольныеСуммы.РежимВыключено()); + +КонецПроцедуры + +Процедура ТестДолжен_ПринятьСовпавшуюСумму() Экспорт + + Заголовки = Новый Соответствие(); + Заголовки.Вставить("X-Checksum-Sha256", СуммаПробы); + + КонтрольныеСуммы.ПроверитьЗагрузку(ФайлПробы, Заголовки, "проба.ospx", КонтрольныеСуммы.РежимСтрогий()); + +КонецПроцедуры + +Процедура ОжидатьОтказ(Знач Заголовки, Знач Режим, Знач ФрагментСообщения) + + Попытка + КонтрольныеСуммы.ПроверитьЗагрузку(ФайлПробы, Заголовки, "проба.ospx", Режим); + Исключение + Ожидаем.Что(ИнформацияОбОшибке().Описание, "сообщение об ошибке").Содержит(ФрагментСообщения); + Возврат; + КонецПопытки; + + юТест.ПрерватьТест(СтрШаблон("Ожидали отказ проверки в режиме <%1>", Режим)); + +КонецПроцедуры + +Функция ШаблоннаяСумма(Знач Символ) + Возврат СтрСоединить(Новый Массив(65), Символ); +КонецФункции diff --git "a/tests/fixtures/\320\226\321\203\321\200\320\275\320\260\320\273\320\270\321\200\321\203\321\216\321\211\320\270\320\271\320\245\320\260\320\261.os" "b/tests/fixtures/\320\226\321\203\321\200\320\275\320\260\320\273\320\270\321\200\321\203\321\216\321\211\320\270\320\271\320\245\320\260\320\261.os" new file mode 100644 index 0000000..be83dde --- /dev/null +++ "b/tests/fixtures/\320\226\321\203\321\200\320\275\320\260\320\273\320\270\321\200\321\203\321\216\321\211\320\270\320\271\320\245\320\260\320\261.os" @@ -0,0 +1,159 @@ +// Журналирующий хаб: локальный сервер на python, который принимает публикацию, отвечает 200 +// и пишет строку каждого запроса в файл. Нужен там, где проверяемое видно только на проводе. + +Перем Порт Экспорт; + +Перем Журнал; +Перем ФайлПорта; +Перем Процесс; + +// Есть ли чем поднять стенд: своего HTTP-сервера у тестов нет, а python есть не везде. +// +// Возвращаемое значение: +// Булево. +// +Функция Доступен() Экспорт + + СистемнаяИнформация = Новый СистемнаяИнформация(); + Если Найти(СистемнаяИнформация.ВерсияОС, "Windows") > 0 Тогда + Возврат Ложь; + КонецЕсли; + + Проба = СоздатьПроцесс("python3 --version", , Истина); + + Попытка + Проба.Запустить(); + Проба.ОжидатьЗавершения(); + Возврат Проба.КодВозврата = 0; + Исключение + Возврат Ложь; + КонецПопытки; + +КонецФункции + +// Поднимает сервер на свободном порту и ждёт, пока тот сообщит, какой порт занял. +// +// Параметры: +// КаталогСтенда - Строка - каталог для сценария, журнала и файла порта. +// +Процедура Поднять(Знач КаталогСтенда) Экспорт + + СоздатьКаталог(КаталогСтенда); + + Журнал = ОбъединитьПути(КаталогСтенда, "запросы.log"); + ФайлПорта = ОбъединитьПути(КаталогСтенда, "порт.txt"); + + ПутьСценария = ОбъединитьПути(КаталогСтенда, "хаб.py"); + Запись = Новый ЗаписьТекста(ПутьСценария, КодировкаТекста.UTF8); + Запись.Записать(СценарийХаба()); + Запись.Закрыть(); + + Процесс = СоздатьПроцесс(СтрШаблон("python3 ""%1"" ""%2"" ""%3""", + ПутьСценария, Журнал, ФайлПорта), , Истина); + Процесс.Запустить(); + + Попытка + Порт = ДождатьсяПорта(); + Исключение + Остановить(); + ВызватьИсключение; + КонецПопытки; + +КонецПроцедуры + +Процедура Остановить() Экспорт + + Если Процесс = Неопределено Тогда + Возврат; + КонецЕсли; + + Попытка + Процесс.Завершить(); + Исключение + КонецПопытки; + + Процесс = Неопределено; + +КонецПроцедуры + +// Запросы, пришедшие на стенд, в порядке поступления. +// +// Возвращаемое значение: +// Массив из Строка - метод и строка запроса, например "POST /api/v1/pools/corp/push?force=1". +// +Функция Запросы() Экспорт + + Результат = Новый Массив(); + + Если НЕ Новый Файл(Журнал).Существует() Тогда + Возврат Результат; + КонецЕсли; + + Чтение = Новый ЧтениеТекста(Журнал); + Содержимое = Чтение.Прочитать(); + Чтение.Закрыть(); + + Для Каждого Строка_ Из СтрРазделить(Строка(Содержимое), Символы.ПС, Ложь) Цикл + Если НЕ ПустаяСтрока(СокрЛП(Строка_)) Тогда + Результат.Добавить(СокрЛП(Строка_)); + КонецЕсли; + КонецЦикла; + + Возврат Результат; + +КонецФункции + +/////////////////////////////////////////////////////////////////// + +Функция ДождатьсяПорта() + + Для НомерПопытки = 1 По 100 Цикл + + Если Новый Файл(ФайлПорта).Существует() Тогда + Чтение = Новый ЧтениеТекста(ФайлПорта); + Значение = СокрЛП(Строка(Чтение.Прочитать())); + Чтение.Закрыть(); + Если ЗначениеЗаполнено(Значение) Тогда + Возврат Число(Значение); + КонецЕсли; + КонецЕсли; + + Приостановить(100); + + КонецЦикла; + + ВызватьИсключение "Журналирующий хаб не сообщил порт"; + +КонецФункции + +// Тело публикации вычитывается до ответа: иначе клиент получит разрыв соединения +// вместо кода состояния и об отправленном адресе сказать будет нечего. +Функция СценарийХаба() + + Возврат "import http.server, socketserver, sys + | + |журнал, файл_порта = sys.argv[1], sys.argv[2] + | + |class Обработчик(http.server.BaseHTTPRequestHandler): + | def do_POST(self): + | self.rfile.read(int(self.headers.get('Content-Length') or 0)) + | with open(журнал, 'a', encoding='utf-8') as f: + | f.write('POST ' + self.path + chr(10)) + | тело = 'пакет опубликован'.encode('utf-8') + | self.send_response(200) + | self.send_header('Content-Length', str(len(тело))) + | self.end_headers() + | self.wfile.write(тело) + | def log_message(self, *args): + | pass + | + |socketserver.TCPServer.allow_reuse_address = True + |сервер = socketserver.TCPServer(('127.0.0.1', 0), Обработчик) + |with open(файл_порта, 'w') as f: + | f.write(str(сервер.server_address[1])) + |сервер.serve_forever() + |"; + +КонецФункции + +Порт = 0; diff --git "a/tests/fixtures/\320\227\320\260\320\263\320\273\321\203\321\210\320\272\320\260\320\227\320\260\320\277\321\200\320\276\321\201\321\207\320\270\320\272\320\260.os" "b/tests/fixtures/\320\227\320\260\320\263\320\273\321\203\321\210\320\272\320\260\320\227\320\260\320\277\321\200\320\276\321\201\321\207\320\270\320\272\320\260.os" new file mode 100644 index 0000000..6aeb9b8 --- /dev/null +++ "b/tests/fixtures/\320\227\320\260\320\263\320\273\321\203\321\210\320\272\320\260\320\227\320\260\320\277\321\200\320\276\321\201\321\207\320\270\320\272\320\260.os" @@ -0,0 +1,27 @@ +// Заглушка ЗапросчикHTTP: заранее заданный ответ и журнал запросов — сеть в тестах не нужна. + +Перем Ответ; +Перем Запросы Экспорт; + +// Задаёт ответ, который вернёт следующий запрос. +// +// Параметры: +// Код - Число - код состояния; 0 означает «до сервера не дозвонились». +// Тело - Строка - тело ответа. +// +Процедура Отвечать(Знач Код, Знач Тело) Экспорт + + Ответ = Новый Структура("Код, Тело", Код, Тело); + +КонецПроцедуры + +Функция Получить(Знач Адрес, Знач Заголовки) Экспорт + + Запросы.Добавить(Новый Структура("Адрес, Заголовки", Адрес, Заголовки)); + + Возврат Ответ; + +КонецФункции + +Ответ = Новый Структура("Код, Тело", 200, "{}"); +Запросы = Новый Массив(); diff --git "a/tests/fixtures/\320\227\320\260\320\263\320\273\321\203\321\210\320\272\320\260\320\227\320\260\320\277\321\203\321\201\320\272\320\260\321\202\320\265\320\273\321\217.os" "b/tests/fixtures/\320\227\320\260\320\263\320\273\321\203\321\210\320\272\320\260\320\227\320\260\320\277\321\203\321\201\320\272\320\260\321\202\320\265\320\273\321\217.os" new file mode 100644 index 0000000..398e708 --- /dev/null +++ "b/tests/fixtures/\320\227\320\260\320\263\320\273\321\203\321\210\320\272\320\260\320\227\320\260\320\277\321\203\321\201\320\272\320\260\321\202\320\265\320\273\321\217.os" @@ -0,0 +1,46 @@ +// Заглушка ЗапускательПрограмм: набор «установленных» команд и журнал запусков. + +Перем Установленные; +Перем Сбои; +// Массив из Структура: Команда (Строка), Параметры (Массив из Строка). +Перем Запуски Экспорт; + +// Объявляет команду установленной. +// +// Параметры: +// Команда - Строка - имя исполняемого. +// КодВозврата - Число - что вернёт запуск. +// Ошибка - Строка - текст сбоя запуска, если запустить не удаётся вовсе. +// +Процедура Установить(Знач Команда, Знач КодВозврата = 0, Знач Ошибка = "") Экспорт + + Установленные.Вставить(Команда, КодВозврата); + Сбои.Вставить(Команда, Ошибка); + +КонецПроцедуры + +Функция Запустить(Знач ИмяКоманды, Знач Параметры = Неопределено) Экспорт + + Запуски.Добавить(Новый Структура("Команда, Параметры", + ИмяКоманды, ?(Параметры = Неопределено, Новый Массив, Параметры))); + + КодВозврата = Установленные.Получить(ИмяКоманды); + + Если КодВозврата = Неопределено Тогда + // команда не заявлена — моделируем «средство не запустилось» + Возврат Новый Структура("КодВозврата, Ошибка, Вывод", -1, "команда не заявлена", ""); + КонецЕсли; + + Ошибка = Строка(?(Сбои.Получить(ИмяКоманды) = Неопределено, "", Сбои.Получить(ИмяКоманды))); + + Если НЕ ПустаяСтрока(Ошибка) Тогда + Возврат Новый Структура("КодВозврата, Ошибка, Вывод", -1, Ошибка, ""); + КонецЕсли; + + Возврат Новый Структура("КодВозврата, Ошибка, Вывод", КодВозврата, "", ""); + +КонецФункции + +Установленные = Новый Соответствие(); +Сбои = Новый Соответствие(); +Запуски = Новый Массив(); diff --git "a/tests/fixtures/\320\227\320\260\320\263\320\273\321\203\321\210\320\272\320\260\320\232\320\276\320\274\320\260\320\275\320\264\321\213\320\237\321\200\320\270\320\273\320\276\320\266\320\265\320\275\320\270\321\217.os" "b/tests/fixtures/\320\227\320\260\320\263\320\273\321\203\321\210\320\272\320\260\320\232\320\276\320\274\320\260\320\275\320\264\321\213\320\237\321\200\320\270\320\273\320\276\320\266\320\265\320\275\320\270\321\217.os" new file mode 100644 index 0000000..4b00216 --- /dev/null +++ "b/tests/fixtures/\320\227\320\260\320\263\320\273\321\203\321\210\320\272\320\260\320\232\320\276\320\274\320\260\320\275\320\264\321\213\320\237\321\200\320\270\320\273\320\276\320\266\320\265\320\275\320\270\321\217.os" @@ -0,0 +1,46 @@ +// Заглушка команды приложения cli: заранее заданные значения опций и аргументов. +// Нужна, чтобы проверять сам ВыполнитьКоманду, а не только его составные части. + +Перем Опции; +Перем Аргументы; + +// Задаёт значение опции. +// +// Параметры: +// Имя - Строка - имя опции без дефисов. +// Значение - Произвольный - значение, которое вернёт ЗначениеОпции. +// +Процедура ЗадатьОпцию(Знач Имя, Знач Значение) Экспорт + + Опции.Вставить(Имя, Значение); + +КонецПроцедуры + +// Задаёт значение аргумента. +// +// Параметры: +// Имя - Строка - имя аргумента. +// Значение - Произвольный - значение, которое вернёт ЗначениеАргумента. +// +Процедура ЗадатьАргумент(Знач Имя, Знач Значение) Экспорт + + Аргументы.Вставить(Имя, Значение); + +КонецПроцедуры + +Функция ЗначениеОпции(Знач Имя) Экспорт + + Значение = Опции.Получить(Имя); + + Возврат ?(Значение = Неопределено, "", Значение); + +КонецФункции + +Функция ЗначениеАргумента(Знач Имя) Экспорт + + Возврат Аргументы.Получить(Имя); + +КонецФункции + +Опции = Новый Соответствие(); +Аргументы = Новый Соответствие(); diff --git "a/tests/fixtures/\320\227\320\260\320\263\320\273\321\203\321\210\320\272\320\260\320\232\320\276\320\275\320\262\320\265\320\271\320\265\321\200\320\260.os" "b/tests/fixtures/\320\227\320\260\320\263\320\273\321\203\321\210\320\272\320\260\320\232\320\276\320\275\320\262\320\265\320\271\320\265\321\200\320\260.os" new file mode 100644 index 0000000..a8c38db --- /dev/null +++ "b/tests/fixtures/\320\227\320\260\320\263\320\273\321\203\321\210\320\272\320\260\320\232\320\276\320\275\320\262\320\265\320\271\320\265\321\200\320\260.os" @@ -0,0 +1,57 @@ +// Заглушка ТокенКонвейераCI: отвечает заранее заданным вердиктом и считает обращения. + +Перем Обнаружение Экспорт; +Перем Результат Экспорт; +Перем ОбращенийБыло Экспорт; +Перем ПоследняяАудитория Экспорт; +Перем ПоследнееИмяПеременной Экспорт; + +// Задаёт, что заглушка «видит» вокруг себя. +// +// Параметры: +// Найдено - Булево - виден ли конвейер с id-token. +// Причина - Строка - объяснение находки либо её отсутствия. +// ИмяПеременной - Строка - переменная среды, в которой лежит id-token. +// +Процедура Обнаруживать(Знач Найдено, Знач Причина = "проба", Знач ИмяПеременной = "") Экспорт + + Обнаружение = Новый Структура("Найдено, Способ, ИмяПеременной, Причина", + Найдено, ?(ПустаяСтрока(ИмяПеременной), "сервис", "переменная"), ИмяПеременной, Причина); + +КонецПроцедуры + +// Задаёт вердикт, который вернёт получение токена. +// +// Параметры: +// Успех - Булево - удалось ли получить токен. +// Токен - Строка - значение id-token. +// Сообщение - Строка - причина неудачи. +// +Процедура Отдавать(Знач Успех, Знач Токен, Знач Сообщение = "") Экспорт + + Результат = Новый Структура("Успех, Токен, Источник, Сообщение", + Успех, Токен, "заглушка", Сообщение); + +КонецПроцедуры + +Функция Обнаружить(Знач ИмяПеременной = "") Экспорт + + Возврат Обнаружение; + +КонецФункции + +Функция Получить(Знач Аудитория, Знач ИмяПеременной = "") Экспорт + + ОбращенийБыло = ОбращенийБыло + 1; + ПоследняяАудитория = Аудитория; + ПоследнееИмяПеременной = ИмяПеременной; + + Возврат Результат; + +КонецФункции + +ОбращенийБыло = 0; +ПоследняяАудитория = ""; +ПоследнееИмяПеременной = ""; +Обнаруживать(Ложь, "заглушка ничего не видит"); +Результат = Новый Структура("Успех, Токен, Источник, Сообщение", Ложь, "", "заглушка", "не задан вердикт"); diff --git "a/tests/fixtures/\320\227\320\260\320\263\320\273\321\203\321\210\320\272\320\260\320\236\321\202\320\262\320\265\321\202\320\260\320\245\320\260\320\261\320\260.os" "b/tests/fixtures/\320\227\320\260\320\263\320\273\321\203\321\210\320\272\320\260\320\236\321\202\320\262\320\265\321\202\320\260\320\245\320\260\320\261\320\260.os" new file mode 100644 index 0000000..d049477 --- /dev/null +++ "b/tests/fixtures/\320\227\320\260\320\263\320\273\321\203\321\210\320\272\320\260\320\236\321\202\320\262\320\265\321\202\320\260\320\245\320\260\320\261\320\260.os" @@ -0,0 +1,46 @@ +// Заглушка ответа хаба: код, тело и заголовки задаёт тест — сеть в тестах не нужна. + +Перем КодСостояния Экспорт; +Перем Заголовки Экспорт; +Перем Тело; + +// Задаёт ответ, который увидит менеджер получения пакетов. +// Заголовки при этом обнуляются: заглушку переиспользуют в одном тесте, и оставшаяся +// от прошлого ответа контрольная сумма прикрыла бы дефект в её проверке. +// +// Параметры: +// Код - Число - код состояния HTTP. +// ТелоОтвета - Строка - содержимое файла пакета. +// +Процедура Отвечать(Знач Код, Знач ТелоОтвета = "") Экспорт + + КодСостояния = Код; + Тело = ТелоОтвета; + Заголовки = Новый Соответствие(); + +КонецПроцедуры + +// Объявляет контрольную сумму так, как это делает хаб. +// +// Параметры: +// Сумма - Строка - значение заголовка X-Checksum-Sha256. +// +Процедура ОбъявитьКонтрольнуюСумму(Знач Сумма) Экспорт + + Заголовки.Вставить("X-Checksum-Sha256", Сумма); + +КонецПроцедуры + +Функция ПолучитьТелоКакДвоичныеДанные() Экспорт + + Возврат ПолучитьДвоичныеДанныеИзСтроки(Тело); + +КонецФункции + +Процедура Закрыть() Экспорт + // для совместимости контракта +КонецПроцедуры + +КодСостояния = 200; +Заголовки = Новый Соответствие(); +Тело = ""; diff --git "a/tests/fixtures/\320\227\320\260\320\263\320\273\321\203\321\210\320\272\320\260\320\241\320\265\321\200\320\262\320\265\321\200\320\260\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" "b/tests/fixtures/\320\227\320\260\320\263\320\273\321\203\321\210\320\272\320\260\320\241\320\265\321\200\320\262\320\265\321\200\320\260\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" new file mode 100644 index 0000000..5806ee3 --- /dev/null +++ "b/tests/fixtures/\320\227\320\260\320\263\320\273\321\203\321\210\320\272\320\260\320\241\320\265\321\200\320\262\320\265\321\200\320\260\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" @@ -0,0 +1,85 @@ +// Заглушка сервера пакетов: журнал запросов и заранее заданный ответ — сеть в тестах не нужна. + +// Поля, которые НастройкиOpm читают у описанного сервера напрямую: заглушка стоит +// в том же списке серверов пакетов и обязана отвечать на весь его контракт. +Перем Имя Экспорт; +Перем Сервер Экспорт; +Перем Порт Экспорт; +Перем ПутьНаСервере Экспорт; +Перем Авторизация Экспорт; + +Перем Пакеты; +Перем Ответ; +Перем Запросы Экспорт; + +Процедура ПриСозданииОбъекта(Знач ИмяСервера = "ЗаглушкаХаба") + + Имя = ИмяСервера; + Сервер = "http://заглушка.хаба"; + Порт = 80; + ПутьНаСервере = "/download/"; + Авторизация = ""; + Пакеты = Новый Соответствие(); + Запросы = Новый Массив(); + +КонецПроцедуры + +// Заявляет пакет в списке пакетов хаба. +// +// Параметры: +// ИмяПакета - Строка - имя пакета, каким его знает хаб. +// +Процедура ОбъявитьПакет(Знач ИмяПакета) Экспорт + + Пакеты.Вставить(ИмяПакета, ""); + +КонецПроцедуры + +// Задаёт ответ на следующие запросы файла пакета. +// +// Параметры: +// ОтветХаба - ЗаглушкаОтветаХаба - ответ, который вернёт ПолучитьРесурс. +// +Процедура Отвечать(Знач ОтветХаба) Экспорт + + Ответ = ОтветХаба; + +КонецПроцедуры + +// Последний запрос, пришедший на сервер. +// +// Возвращаемое значение: +// Структура - ИмяРесурса (Строка), Спецификация (Строка). +// +Функция ПоследнийЗапрос() Экспорт + + Если Запросы.Количество() = 0 Тогда + ВызватьИсключение "Заглушка хаба запросов не получала"; + КонецЕсли; + + Возврат Запросы[Запросы.Количество() - 1]; + +КонецФункции + +/////////////////////////////////////////////////////////////////// +// Контракт сервера пакетов + +Функция ПолучитьИмя() Экспорт + Возврат Имя; +КонецФункции + +Функция СерверДоступен() Экспорт + Возврат Истина; +КонецФункции + +Функция ПолучитьПакеты() Экспорт + Возврат Пакеты; +КонецФункции + +Функция ПолучитьРесурс(Знач ИмяРесурса, Знач Спецификация = Неопределено) Экспорт + + Запросы.Добавить(Новый Структура("ИмяРесурса, Спецификация", ИмяРесурса, Спецификация)); + + Возврат Ответ; + +КонецФункции diff --git a/tests/hub-address-test.os b/tests/hub-address-test.os new file mode 100644 index 0000000..7a232ae --- /dev/null +++ b/tests/hub-address-test.os @@ -0,0 +1,281 @@ +#Использовать asserts +#Использовать "../src/core" + +Перем юТест; + +Функция ПолучитьСписокТестов(Знач Тестирование) Экспорт + + юТест = Тестирование; + + СписокТестов = Новый Массив; + + СписокТестов.Добавить("ТестДолжен_ОтличитьАдресОтИмениПакета"); + СписокТестов.Добавить("ТестДолжен_СчитатьАдресомХостСПортомБезПула"); + СписокТестов.Добавить("ТестДолжен_НеСчитатьАдресомОтносительныйПуть"); + СписокТестов.Добавить("ТестДолжен_РазобратьАдресПакетаВПулеПоУмолчанию"); + СписокТестов.Добавить("ТестДолжен_РазобратьАдресПакетаСПулом"); + СписокТестов.Добавить("ТестДолжен_ОтказатьНаАдресеПакетаБезИмени"); + СписокТестов.Добавить("ТестДолжен_РазобратьАдресСПуломПакетомИТегом"); + СписокТестов.Добавить("ТестДолжен_РазобратьАдресСЯвнойСхемой"); + СписокТестов.Добавить("ТестДолжен_ВзятьHttpДляЛокальногоХоста"); + СписокТестов.Добавить("ТестДолжен_ВзятьHttpДляАдресаСПортом"); + СписокТестов.Добавить("ТестДолжен_ВзятьHttpsДляПубличногоИмениБезПорта"); + СписокТестов.Добавить("ТестДолжен_НеДобавлятьПортПоУмолчаниюВБазовыйАдрес"); + СписокТестов.Добавить("ТестДолжен_РазобратьАдресТолькоСХостом"); + СписокТестов.Добавить("ТестДолжен_РазобратьIPv6Хост"); + СписокТестов.Добавить("ТестДолжен_ОтброситьХвостПослеВопросаИРешётки"); + СписокТестов.Добавить("ТестДолжен_ОтказатьНаНеизвестнойСхеме"); + СписокТестов.Добавить("ТестДолжен_ОтказатьНаНечисловомПорте"); + СписокТестов.Добавить("ТестДолжен_ОтказатьНаЛишнихСегментах"); + СписокТестов.Добавить("ТестДолжен_ОтказатьНаПустомАдресе"); + СписокТестов.Добавить("ТестДолжен_СобратьПутиВыдачи"); + СписокТестов.Добавить("ТестДолжен_СобратьПутиПубликации"); + СписокТестов.Добавить("ТестДолжен_УзнатьПулПоПутиПубликации"); + СписокТестов.Добавить("ТестДолжен_ОтличитьПустуюСпецификациюАдресаОтОтсутствующей"); + + Возврат СписокТестов; + +КонецФункции + +Процедура ПередЗапускомТеста() Экспорт +КонецПроцедуры + +Процедура ПослеЗапускаТеста() Экспорт +КонецПроцедуры + +Процедура ТестДолжен_ОтличитьАдресОтИмениПакета() Экспорт + + Ожидаем.Что(АдресаХаба.ЭтоАдресХаба("localhost:3380/corp/my-package@stable"), "адрес с пулом").Равно(Истина); + Ожидаем.Что(АдресаХаба.ЭтоАдресХаба("http://hub.example.com"), "адрес со схемой").Равно(Истина); + Ожидаем.Что(АдресаХаба.ЭтоАдресХаба("my-package"), "голое имя пакета").Равно(Ложь); + Ожидаем.Что(АдресаХаба.ЭтоАдресХаба("my-package@stable"), "имя пакета с тегом").Равно(Ложь); + Ожидаем.Что(АдресаХаба.ЭтоАдресХаба("dist/my-package-1.0.0.ospx"), "файл пакета").Равно(Ложь); + Ожидаем.Что(АдресаХаба.ЭтоАдресХаба(""), "пустая строка").Равно(Ложь); + +КонецПроцедуры + +// Хост без пула — полноценный адрес: он означает пул хаба по умолчанию. +Процедура ТестДолжен_СчитатьАдресомХостСПортомБезПула() Экспорт + + Ожидаем.Что(АдресаХаба.ЭтоАдресХаба("localhost:3380"), "хост с портом").Равно(Истина); + Ожидаем.Что(АдресаХаба.ЭтоАдресХаба("127.0.0.1:3380"), "IPv4 с портом").Равно(Истина); + Ожидаем.Что(АдресаХаба.ЭтоАдресХаба("hub.example.com:8080"), "домен с портом").Равно(Истина); + Ожидаем.Что(АдресаХаба.ЭтоАдресХаба("localhost/corp"), "localhost с пулом").Равно(Истина); + Ожидаем.Что(АдресаХаба.ЭтоАдресХаба("hub.example.com/corp"), "домен с пулом").Равно(Истина); + Ожидаем.Что(АдресаХаба.ЭтоАдресХаба("http://hub/corp"), "короткое имя со схемой").Равно(Истина); + +КонецПроцедуры + +// Ложные срабатывания: относительный путь и голое доменное имя адресом не считаются. +Процедура ТестДолжен_НеСчитатьАдресомОтносительныйПуть() Экспорт + + Ожидаем.Что(АдресаХаба.ЭтоАдресХаба("dist/pkg"), "каталог сборки без расширения").Равно(Ложь); + Ожидаем.Что(АдресаХаба.ЭтоАдресХаба("out/build/pkg"), "вложенный путь").Равно(Ложь); + Ожидаем.Что(АдресаХаба.ЭтоАдресХаба("hub.example.com"), "голое доменное имя без порта").Равно(Ложь); + Ожидаем.Что(АдресаХаба.ЭтоАдресХаба("my-package-1.0.0"), "имя файла без расширения").Равно(Ложь); + Ожидаем.Что(АдресаХаба.ЭтоАдресХаба("dist/my-package.OSPX"), "расширение в верхнем регистре").Равно(Ложь); + +КонецПроцедуры + +Процедура ТестДолжен_РазобратьАдресПакетаВПулеПоУмолчанию() Экспорт + + Адрес = АдресаХаба.РазобратьАдресПакета("localhost:3380/asserts@stable"); + + Ожидаем.Что(Адрес.Пул, "пул по умолчанию не назван").Равно(""); + Ожидаем.Что(Адрес.ИмяПакета, "имя пакета").Равно("asserts"); + Ожидаем.Что(Адрес.Спецификация, "тег").Равно("stable"); + +КонецПроцедуры + +Процедура ТестДолжен_РазобратьАдресПакетаСПулом() Экспорт + + Адрес = АдресаХаба.РазобратьАдресПакета("localhost:3380/corp/veles-core@1.2.0"); + + Ожидаем.Что(Адрес.Пул, "пул").Равно("corp"); + Ожидаем.Что(Адрес.ИмяПакета, "имя пакета").Равно("veles-core"); + Ожидаем.Что(Адрес.Спецификация, "версия").Равно("1.2.0"); + +КонецПроцедуры + +Процедура ТестДолжен_ОтказатьНаАдресеПакетаБезИмени() Экспорт + + Попытка + АдресаХаба.РазобратьАдресПакета("localhost:3380"); + Исключение + Ожидаем.Что(ИнформацияОбОшибке().Описание, "сообщение об ошибке").Содержит("не указано имя пакета"); + Возврат; + КонецПопытки; + + юТест.ПрерватьТест("Ожидали исключение на адресе без имени пакета"); + +КонецПроцедуры + +Процедура ТестДолжен_РазобратьАдресСПуломПакетомИТегом() Экспорт + + Адрес = АдресаХаба.Разобрать("localhost:3380/corp/my-package@stable"); + + Ожидаем.Что(Адрес.Схема, "схема").Равно("http"); + Ожидаем.Что(Адрес.Хост, "хост").Равно("localhost"); + Ожидаем.Что(Адрес.Порт, "порт").Равно(3380); + Ожидаем.Что(Адрес.Пул, "пул").Равно("corp"); + Ожидаем.Что(Адрес.ИмяПакета, "имя пакета").Равно("my-package"); + Ожидаем.Что(Адрес.Спецификация, "спецификация").Равно("stable"); + Ожидаем.Что(Адрес.БазовыйАдрес, "базовый адрес").Равно("http://localhost:3380"); + +КонецПроцедуры + +Процедура ТестДолжен_РазобратьАдресСЯвнойСхемой() Экспорт + + Адрес = АдресаХаба.Разобрать("https://hub.example.com/team/pkg@1.2.3"); + + Ожидаем.Что(Адрес.Схема, "схема").Равно("https"); + Ожидаем.Что(Адрес.СхемаЯвная, "схема указана явно").Равно(Истина); + Ожидаем.Что(Адрес.Пул, "пул").Равно("team"); + Ожидаем.Что(Адрес.ИмяПакета, "имя пакета").Равно("pkg"); + Ожидаем.Что(Адрес.Спецификация, "версия").Равно("1.2.3"); + +КонецПроцедуры + +Процедура ТестДолжен_ВзятьHttpДляЛокальногоХоста() Экспорт + + Ожидаем.Что(АдресаХаба.Разобрать("localhost/corp").Схема, "localhost").Равно("http"); + Ожидаем.Что(АдресаХаба.Разобрать("127.0.0.1/corp").Схема, "127.0.0.1").Равно("http"); + Ожидаем.Что(АдресаХаба.Разобрать("hub.localhost/corp").Схема, "поддомен .localhost").Равно("http"); + +КонецПроцедуры + +Процедура ТестДолжен_ВзятьHttpДляАдресаСПортом() Экспорт + + Адрес = АдресаХаба.Разобрать("hub.example.com:8080/corp"); + + Ожидаем.Что(Адрес.Схема, "схема").Равно("http"); + Ожидаем.Что(Адрес.ПортЯвный, "порт указан явно").Равно(Истина); + +КонецПроцедуры + +Процедура ТестДолжен_ВзятьHttpsДляПубличногоИмениБезПорта() Экспорт + + Адрес = АдресаХаба.Разобрать("hub.oscript.io/public/asserts"); + + Ожидаем.Что(Адрес.Схема, "схема").Равно("https"); + Ожидаем.Что(Адрес.ХостЛокальный, "хост не локальный").Равно(Ложь); + Ожидаем.Что(Адрес.БазовыйАдрес, "базовый адрес").Равно("https://hub.oscript.io"); + +КонецПроцедуры + +Процедура ТестДолжен_НеДобавлятьПортПоУмолчаниюВБазовыйАдрес() Экспорт + + Ожидаем.Что(АдресаХаба.Разобрать("http://hub.example.com:80/corp").БазовыйАдрес, "http и 80") + .Равно("http://hub.example.com"); + Ожидаем.Что(АдресаХаба.Разобрать("https://hub.example.com:443/corp").БазовыйАдрес, "https и 443") + .Равно("https://hub.example.com"); + +КонецПроцедуры + +Процедура ТестДолжен_РазобратьАдресТолькоСХостом() Экспорт + + Адрес = АдресаХаба.Разобрать("http://localhost:3380"); + + Ожидаем.Что(Адрес.Пул, "пул пустой").Равно(""); + Ожидаем.Что(Адрес.ИмяПакета, "имя пакета пустое").Равно(""); + Ожидаем.Что(Адрес.Спецификация, "спецификация не названа").Равно(Неопределено); + +КонецПроцедуры + +Процедура ТестДолжен_РазобратьIPv6Хост() Экспорт + + Адрес = АдресаХаба.Разобрать("http://[::1]:3380/corp"); + + Ожидаем.Что(Адрес.Хост, "хост").Равно("[::1]"); + Ожидаем.Что(Адрес.Порт, "порт").Равно(3380); + Ожидаем.Что(Адрес.ХостЛокальный, "локальный хост").Равно(Истина); + +КонецПроцедуры + +Процедура ТестДолжен_ОтброситьХвостПослеВопросаИРешётки() Экспорт + + Адрес = АдресаХаба.Разобрать("http://localhost:3380/corp/pkg@stable?x=1#y"); + + Ожидаем.Что(Адрес.Пул, "пул").Равно("corp"); + Ожидаем.Что(Адрес.ИмяПакета, "имя пакета").Равно("pkg"); + Ожидаем.Что(Адрес.Спецификация, "спецификация").Равно("stable"); + +КонецПроцедуры + +Процедура ТестДолжен_ОтказатьНаНеизвестнойСхеме() Экспорт + + ОжидатьИсключение("ftp://hub.example.com/corp", "схем"); + +КонецПроцедуры + +Процедура ТестДолжен_ОтказатьНаНечисловомПорте() Экспорт + + ОжидатьИсключение("localhost:порт/corp", "порт"); + +КонецПроцедуры + +Процедура ТестДолжен_ОтказатьНаЛишнихСегментах() Экспорт + + ОжидатьИсключение("localhost:3380/corp/pkg/лишнее", "ожидалось"); + +КонецПроцедуры + +Процедура ТестДолжен_ОтказатьНаПустомАдресе() Экспорт + + ОжидатьИсключение(" ", "Пустой адрес"); + +КонецПроцедуры + +Процедура ТестДолжен_СобратьПутиВыдачи() Экспорт + + Ожидаем.Что(АдресаХаба.ПутьВыдачи("corp"), "пул").Равно("/api/v1/pools/corp/download/"); + Ожидаем.Что(АдресаХаба.ПутьВыдачи(""), "пул по умолчанию: легаси-путь").Равно("/download/"); + +КонецПроцедуры + +Процедура ТестДолжен_СобратьПутиПубликации() Экспорт + + Ожидаем.Что(АдресаХаба.ПутьПубликации("corp"), "публикация в пул").Равно("/api/v1/pools/corp/push"); + Ожидаем.Что(АдресаХаба.ПутьПубликации(""), "легаси-публикация").Равно("/push"); + +КонецПроцедуры + +// Обратная сторона ПутьПубликации: по описанному в opm.cfg серверу видно, публикуем ли +// мы в пул OpenHub — от этого зависит, обязателен ли канал версии. +Процедура ТестДолжен_УзнатьПулПоПутиПубликации() Экспорт + + Ожидаем.Что(АдресаХаба.ПулИзПутиПубликации(АдресаХаба.ПутьПубликации("corp")), "пул из пути") + .Равно("corp"); + Ожидаем.Что(АдресаХаба.ПулИзПутиПубликации("/push"), "легаси-путь пула не несёт").Равно(""); + Ожидаем.Что(АдресаХаба.ПулИзПутиПубликации(""), "пустой путь").Равно(""); + Ожидаем.Что(АдресаХаба.ПулИзПутиПубликации("/api/v1/pools/"), "пул не назван").Равно(""); + +КонецПроцедуры + +// «пакет» отдаёт выбор версии хабу, «пакет@» спецификацию назвал и оставил пустой. +// Схлопни адресная форма эти два случая — и на опечатку тихо встанет версия хаба, +// хотя классическая форма команды на том же «пакет@» упирается в отказ. +Процедура ТестДолжен_ОтличитьПустуюСпецификациюАдресаОтОтсутствующей() Экспорт + + Ожидаем.Что(АдресаХаба.РазобратьАдресПакета("localhost:3380/vrunner").Спецификация, + "собаки нет — версию выбирает хаб").Равно(Неопределено); + Ожидаем.Что(АдресаХаба.РазобратьАдресПакета("localhost:3380/vrunner@").Спецификация, + "собака есть, спецификации нет").Равно(""); + Ожидаем.Что(АдресаХаба.РазобратьАдресПакета("localhost:3380/corp/vrunner@").Спецификация, + "то же самое в адресе с пулом").Равно(""); + +КонецПроцедуры + +Процедура ОжидатьИсключение(Знач Адрес, Знач ФрагментСообщения) + + Попытка + АдресаХаба.Разобрать(Адрес); + Исключение + Ожидаем.Что(ИнформацияОбОшибке().Описание, СтрШаблон("сообщение об ошибке для <%1>", Адрес)) + .Содержит(ФрагментСообщения); + Возврат; + КонецПопытки; + + юТест.ПрерватьТест(СтрШаблон("Ожидали исключение на адресе <%1>", Адрес)); + +КонецПроцедуры diff --git a/tests/hub-login-config-test.os b/tests/hub-login-config-test.os new file mode 100644 index 0000000..3cd38ee --- /dev/null +++ b/tests/hub-login-config-test.os @@ -0,0 +1,244 @@ +#Использовать asserts +#Использовать fs +#Использовать tempfiles +#Использовать 1commands +#Использовать "../src/core" + +Перем юТест; +Перем ФайлНастроек; + +Функция ПолучитьСписокТестов(Знач Тестирование) Экспорт + + юТест = Тестирование; + + СписокТестов = Новый Массив; + + СписокТестов.Добавить("ТестДолжен_ВывестиИмяСервераПоАдресу"); + СписокТестов.Добавить("ТестДолжен_ЗаписатьСерверПакетовВПустыеНастройки"); + СписокТестов.Добавить("ТестДолжен_ОбновитьЗаписьПриПовторномВходе"); + СписокТестов.Добавить("ТестДолжен_ДобавитьОтдельнуюЗаписьДляДругогоПула"); + СписокТестов.Добавить("ТестДолжен_СохранитьЧужиеНастройкиПриЗаписи"); + СписокТестов.Добавить("ТестДолжен_УдалитьЗаписьПриВыходе"); + СписокТестов.Добавить("ТестДолжен_НайтиАвторизациюПоАдресуСПортомВАдресеСервера"); + СписокТестов.Добавить("ТестДолжен_НеНайтиАвторизациюДляЧужогоПорта"); + СписокТестов.Добавить("ТестДолжен_НеНайтиАвторизациюДляЧужойСхемы"); + СписокТестов.Добавить("ТестДолжен_РазличитьХабыОдногоХостаНаРазныхПортах"); + СписокТестов.Добавить("ТестДолжен_УдалитьПриВыходеТолькоЗаписьСвоегоПорта"); + + Если НЕ ЭтоWindows() Тогда + СписокТестов.Добавить("ТестДолжен_ЗакрытьФайлНастроекОтПосторонних"); + КонецЕсли; + + Возврат СписокТестов; + +КонецФункции + +Процедура ПередЗапускомТеста() Экспорт + + ФайлНастроек = ВременныеФайлы.НовоеИмяФайла("cfg"); + НастройкиOpm.СброситьНастройки(); + +КонецПроцедуры + +Процедура ПослеЗапускаТеста() Экспорт + + НастройкиOpm.СброситьНастройки(); + ВременныеФайлы.Удалить(); + +КонецПроцедуры + +/////////////////////////////////////////////////////////////////// + +Функция ЭтоWindows() + + СистемнаяИнформация = Новый СистемнаяИнформация(); + + Возврат Найти(НРег(СистемнаяИнформация.ВерсияОС), "windows") > 0; + +КонецФункции + +// Первые десять символов вывода ls -l — режим доступа; macOS дописывает после них +// «@» или «+» для расширенных атрибутов, поэтому берём ровно режим. +Функция ПраваФайла(Знач ПутьКФайлу) + + Команда = Новый Команда(); + Команда.УстановитьКоманду("ls"); + Команда.ДобавитьПараметр("-l"); + Команда.ДобавитьПараметр(ПутьКФайлу); + Команда.ПоказыватьВыводНемедленно(Ложь); + Команда.Исполнить(); + + Возврат Лев(СокрЛП(Команда.ПолучитьВывод()), 10); + +КонецФункции + +Процедура ОткрытьДоступВсем(Знач ПутьКФайлу) + + Команда = Новый Команда(); + Команда.УстановитьКоманду("chmod"); + Команда.ДобавитьПараметр("644"); + Команда.ДобавитьПараметр(ПутьКФайлу); + Команда.ПоказыватьВыводНемедленно(Ложь); + Команда.Исполнить(); + +КонецПроцедуры + +/////////////////////////////////////////////////////////////////// + +Процедура ТестДолжен_ВывестиИмяСервераПоАдресу() Экспорт + + Ожидаем.Что(ФайлыНастроекOpm.ИмяСервераДляЗаписи(АдресаХаба.Разобрать("localhost:3380/corp")), "хост, порт и пул") + .Равно("localhost-3380-corp"); + Ожидаем.Что(ФайлыНастроекOpm.ИмяСервераДляЗаписи(АдресаХаба.Разобрать("https://hub.example.com")), "только хост") + .Равно("hub.example.com"); + +КонецПроцедуры + +Процедура ТестДолжен_ЗаписатьСерверПакетовВПустыеНастройки() Экспорт + + Настройки = Новый Структура(); + ИмяСервера = ФайлыНастроекOpm.ЗаписатьСерверПакетов(Настройки, АдресаХаба.Разобрать("localhost:3380/corp"), "oshub_1_секрет"); + + Ожидаем.Что(ИмяСервера, "имя сервера").Равно("localhost-3380-corp"); + Ожидаем.Что(Настройки.СервераПакетов.Количество(), "число серверов").Равно(1); + + Запись = Настройки.СервераПакетов[0]; + Ожидаем.Что(Запись["Сервер"], "адрес сервера").Равно("http://localhost"); + Ожидаем.Что(Запись["Порт"], "порт").Равно(3380); + Ожидаем.Что(Запись["ПутьНаСервере"], "путь выдачи").Равно("/api/v1/pools/corp/download/"); + Ожидаем.Что(Запись["РесурсПубликацииПакетов"], "путь публикации").Равно("/api/v1/pools/corp/push"); + Ожидаем.Что(Запись["Авторизация"], "заголовок авторизации").Равно("Bearer oshub_1_секрет"); + +КонецПроцедуры + +Процедура ТестДолжен_ОбновитьЗаписьПриПовторномВходе() Экспорт + + Адрес = АдресаХаба.Разобрать("localhost:3380/corp"); + + Настройки = Новый Структура(); + ФайлыНастроекOpm.ЗаписатьСерверПакетов(Настройки, Адрес, "старый"); + ФайлыНастроекOpm.ЗаписатьСерверПакетов(Настройки, Адрес, "новый"); + + Ожидаем.Что(Настройки.СервераПакетов.Количество(), "дублей быть не должно").Равно(1); + Ожидаем.Что(Настройки.СервераПакетов[0]["Авторизация"], "токен обновлён").Равно("Bearer новый"); + +КонецПроцедуры + +Процедура ТестДолжен_ДобавитьОтдельнуюЗаписьДляДругогоПула() Экспорт + + Настройки = Новый Структура(); + ФайлыНастроекOpm.ЗаписатьСерверПакетов(Настройки, АдресаХаба.Разобрать("localhost:3380/corp"), "к1"); + ФайлыНастроекOpm.ЗаписатьСерверПакетов(Настройки, АдресаХаба.Разобрать("localhost:3380/team"), "к2"); + + Ожидаем.Что(Настройки.СервераПакетов.Количество(), "пулы описываются отдельно").Равно(2); + +КонецПроцедуры + +Процедура ТестДолжен_СохранитьЧужиеНастройкиПриЗаписи() Экспорт + + Исходные = Новый Структура(); + Исходные.Вставить("СоздаватьShСкриптЗапуска", Истина); + ФайлыНастроекOpm.ЗаписатьСерверПакетов(Исходные, АдресаХаба.Разобрать("localhost:3380/corp"), "токен"); + ФайлыНастроекOpm.ЗаписатьНастройки(Исходные, ФайлНастроек); + + Прочитанные = ФайлыНастроекOpm.ПрочитатьНастройки(ФайлНастроек); + + Ожидаем.Что(Прочитанные.СоздаватьShСкриптЗапуска, "чужая настройка на месте").Равно(Истина); + Ожидаем.Что(Прочитанные.СервераПакетов.Количество(), "сервер записан").Равно(1); + +КонецПроцедуры + +Процедура ТестДолжен_УдалитьЗаписьПриВыходе() Экспорт + + Адрес = АдресаХаба.Разобрать("localhost:3380/corp"); + + Настройки = Новый Структура(); + ФайлыНастроекOpm.ЗаписатьСерверПакетов(Настройки, Адрес, "токен"); + + Ожидаем.Что(ФайлыНастроекOpm.УдалитьСерверПакетов(Настройки, Адрес), "запись найдена").Равно(Истина); + Ожидаем.Что(Настройки.СервераПакетов.Количество(), "запись удалена").Равно(0); + Ожидаем.Что(ФайлыНастроекOpm.УдалитьСерверПакетов(Настройки, Адрес), "повторный выход").Равно(Ложь); + +КонецПроцедуры + +// Порт часто дублируют внутри адреса сервера — исторически opm push поле Порт не читал. +Процедура ТестДолжен_НайтиАвторизациюПоАдресуСПортомВАдресеСервера() Экспорт + + НастройкиOpm.ДобавитьСерверПакетов(Новый СерверПакетов("демо", "http://localhost:3391", + "/api/v1/pools/corp/download/", "/api/v1/pools/corp/push", 3391, 0, "Bearer oshub_2_секрет")); + + Найденная = НастройкиOpm.АвторизацияДляАдреса(АдресаХаба.Разобрать("localhost:3391/corp")); + + Ожидаем.Что(Найденная, "авторизация подхвачена из настроек").Равно("Bearer oshub_2_секрет"); + +КонецПроцедуры + +Процедура ТестДолжен_НеНайтиАвторизациюДляЧужогоПорта() Экспорт + + НастройкиOpm.ДобавитьСерверПакетов(Новый СерверПакетов("демо", "http://localhost", "/api/v1/pools/corp/download/", + "/api/v1/pools/corp/push", 3391, 0, "Bearer oshub_2_секрет")); + + Найденная = НастройкиOpm.АвторизацияДляАдреса(АдресаХаба.Разобрать("localhost:9999/corp")); + + Ожидаем.Что(Найденная, "чужой порт не даёт токен").Равно(""); + +КонецПроцедуры + +// Схема — часть ключа: иначе вход, сохранённый для https, уехал бы токеном +// в незашифрованный запрос к тому же хосту и порту. +Процедура ТестДолжен_НеНайтиАвторизациюДляЧужойСхемы() Экспорт + + НастройкиOpm.ДобавитьСерверПакетов(Новый СерверПакетов("демо", "https://hub.example.com", + "/api/v1/pools/corp/download/", "/api/v1/pools/corp/push", 443, 0, "Bearer oshub_3_секрет")); + + Ожидаем.Что(НастройкиOpm.АвторизацияДляАдреса(АдресаХаба.Разобрать("http://hub.example.com:443/corp")), + "открытый канал токена не получает").Равно(""); + Ожидаем.Что(НастройкиOpm.АвторизацияДляАдреса(АдресаХаба.Разобрать("https://hub.example.com/corp")), + "своя схема токен получает").Равно("Bearer oshub_3_секрет"); + +КонецПроцедуры + +// Поле Сервер собирается без порта, поэтому без порта в ключе вход в localhost:4400 +// затирал бы токен localhost:3380. +Процедура ТестДолжен_РазличитьХабыОдногоХостаНаРазныхПортах() Экспорт + + Настройки = Новый Структура(); + ФайлыНастроекOpm.ЗаписатьСерверПакетов(Настройки, АдресаХаба.Разобрать("localhost:3380/corp"), "к3380"); + ФайлыНастроекOpm.ЗаписатьСерверПакетов(Настройки, АдресаХаба.Разобрать("localhost:4400/corp"), "к4400"); + + Ожидаем.Что(Настройки.СервераПакетов.Количество(), "хабы разных портов описаны отдельно").Равно(2); + Ожидаем.Что(Настройки.СервераПакетов[0]["Авторизация"], "токен первого хаба на месте").Равно("Bearer к3380"); + Ожидаем.Что(Настройки.СервераПакетов[1]["Авторизация"], "токен второго хаба записан").Равно("Bearer к4400"); + +КонецПроцедуры + +Процедура ТестДолжен_УдалитьПриВыходеТолькоЗаписьСвоегоПорта() Экспорт + + Настройки = Новый Структура(); + ФайлыНастроекOpm.ЗаписатьСерверПакетов(Настройки, АдресаХаба.Разобрать("localhost:3380/corp"), "к3380"); + ФайлыНастроекOpm.ЗаписатьСерверПакетов(Настройки, АдресаХаба.Разобрать("localhost:4400/corp"), "к4400"); + + Ожидаем.Что(ФайлыНастроекOpm.УдалитьСерверПакетов(Настройки, АдресаХаба.Разобрать("localhost:4400/corp")), + "запись найдена").Равно(Истина); + + Ожидаем.Что(Настройки.СервераПакетов.Количество(), "чужая запись осталась").Равно(1); + Ожидаем.Что(Настройки.СервераПакетов[0]["Авторизация"], "осталась запись другого порта").Равно("Bearer к3380"); + +КонецПроцедуры + +// В файле лежит токен, и посторонним он закрыт в обоих случаях: и когда файл заводится +// заново, и когда он уже лежал открытым от прошлого входа. Момент, в который права +// закрываются, тестом не наблюдаем — его стережёт порядок в ЗаписатьНастройки. +Процедура ТестДолжен_ЗакрытьФайлНастроекОтПосторонних() Экспорт + + Настройки = Новый Структура(); + ФайлыНастроекOpm.ЗаписатьСерверПакетов(Настройки, АдресаХаба.Разобрать("localhost:3380/corp"), "секрет"); + + ФайлыНастроекOpm.ЗаписатьНастройки(Настройки, ФайлНастроек); + Ожидаем.Что(ПраваФайла(ФайлНастроек), "новый файл закрыт от посторонних").Равно("-rw-------"); + + ОткрытьДоступВсем(ФайлНастроек); + ФайлыНастроекOpm.ЗаписатьНастройки(Настройки, ФайлНастроек); + Ожидаем.Что(ПраваФайла(ФайлНастроек), "повторный вход закрывает права заново").Равно("-rw-------"); + +КонецПроцедуры diff --git a/tests/login-browser-test.os b/tests/login-browser-test.os new file mode 100644 index 0000000..9e1cb09 --- /dev/null +++ b/tests/login-browser-test.os @@ -0,0 +1,272 @@ +#Использовать asserts +#Использовать tempfiles +#Использовать "../src/core" +#Использовать "../src/cmd" + +Перем юТест; +Перем Запускатель; + +Функция ПолучитьСписокТестов(Знач Тестирование) Экспорт + + юТест = Тестирование; + + СписокТестов = Новый Массив; + + СписокТестов.Добавить("ТестДолжен_ОткрытьЧерезOpenНаMacOS"); + СписокТестов.Добавить("ТестДолжен_ОткрытьЧерезXdgOpenВUnix"); + СписокТестов.Добавить("ТестДолжен_ОткрытьЧерезStartНаWindows"); + СписокТестов.Добавить("ТестДолжен_НазватьПричинуСбояЗапуска"); + СписокТестов.Добавить("ТестДолжен_СказатьЧтоОткрытьНечем"); + СписокТестов.Добавить("ТестДолжен_ОтказатьсяОткрыватьПустойАдрес"); + СписокТестов.Добавить("ТестДолжен_ОпределитьСемействоТекущейПлатформы"); + СписокТестов.Добавить("ТестДолжен_ОтвергнутьАдресНеПоHttp"); + СписокТестов.Добавить("ТестДолжен_ЗакодироватьАдресПередЗапуском"); + СписокТестов.Добавить("ТестДолжен_НеТронутьУжеКодированныйАдрес"); + СписокТестов.Добавить("ТестДолжен_ОтдатьАдресОтдельнымПараметром"); + СписокТестов.Добавить("ТестДолжен_ПрочитатьПеременнуюСредыИзПодставленногоНабора"); + СписокТестов.Добавить("ТестДолжен_ПоказатьАдресСКодомДляПерехода"); + СписокТестов.Добавить("ТестДолжен_ОткатитьсяНаАдресБезКода"); + СписокТестов.Добавить("ТестДолжен_НапечататьПолнуюСсылкуИКодОтдельнойСтрокой"); + + Возврат СписокТестов; + +КонецФункции + +Процедура ПередЗапускомТеста() Экспорт + + Запускатель = ЗагрузитьЗаглушку("ЗаглушкаЗапускателя.os"); + +КонецПроцедуры + +Процедура ПослеЗапускаТеста() Экспорт + + ВременныеФайлы.Удалить(); + +КонецПроцедуры + +//////////////////////////////////////////////////////////////////////////////// +// Выбор команды открытия — по семейству ОС + +Процедура ТестДолжен_ОткрытьЧерезOpenНаMacOS() Экспорт + + Запускатель.Установить("open"); + + Результат = Новый ОткрывательБраузера(Запускатель, "macos").Открыть(АдресПробы()); + + Ожидаем.Что(Результат.Успех, "открытие удалось").ЭтоИстина(); + Ожидаем.Что(Результат.Инструмент, "чем открыто").Равно("open"); + Ожидаем.Что(Запускатель.Запуски.Количество(), "число запусков").Равно(1); + Ожидаем.Что(Запускатель.Запуски[0].Команда, "исполняемое").Равно("open"); + Ожидаем.Что(Параметры(Запускатель.Запуски[0]), "параметры").Равно(АдресПробы()); + +КонецПроцедуры + +Процедура ТестДолжен_ОткрытьЧерезXdgOpenВUnix() Экспорт + + Запускатель.Установить("xdg-open"); + + Результат = Новый ОткрывательБраузера(Запускатель, "unix").Открыть(АдресПробы()); + + Ожидаем.Что(Результат.Успех, "открытие удалось").ЭтоИстина(); + Ожидаем.Что(Результат.Инструмент, "вне macOS команда xdg-open").Равно("xdg-open"); + Ожидаем.Что(Запускатель.Запуски[0].Команда, "исполняемое").Равно("xdg-open"); + Ожидаем.Что(Параметры(Запускатель.Запуски[0]), "параметры").Равно(АдресПробы()); + +КонецПроцедуры + +// start исполняемым файлом не является — это встроенная команда cmd, поэтому +// на Windows запускается сам cmd, а пустые кавычки занимают место заголовка окна. +Процедура ТестДолжен_ОткрытьЧерезStartНаWindows() Экспорт + + Запускатель.Установить("cmd"); + + Результат = Новый ОткрывательБраузера(Запускатель, "windows").Открыть(АдресПробы()); + + Ожидаем.Что(Результат.Успех, "открытие удалось").ЭтоИстина(); + Ожидаем.Что(Результат.Инструмент, "команда Windows").Равно("start"); + Ожидаем.Что(Запускатель.Запуски[0].Команда, "исполняемое").Равно("cmd"); + Ожидаем.Что(Параметры(Запускатель.Запуски[0]), "параметры") + .Равно("/c start """" " + АдресПробы()); + +КонецПроцедуры + +Процедура ТестДолжен_НазватьПричинуСбояЗапуска() Экспорт + + Запускатель.Установить("open", 0, "файл не найден"); + + Результат = Новый ОткрывательБраузера(Запускатель, "macos").Открыть(АдресПробы()); + + Ожидаем.Что(Результат.Успех, "открыть не удалось").ЭтоЛожь(); + Ожидаем.Что(Результат.Сообщение, "названы инструмент и причина") + .Содержит("open не запустился: файл не найден"); + +КонецПроцедуры + +Процедура ТестДолжен_СказатьЧтоОткрытьНечем() Экспорт + + // команда не заявлена — заглушка вернёт «не запустилось» + Результат = Новый ОткрывательБраузера(Запускатель, "macos").Открыть(АдресПробы()); + + Ожидаем.Что(Результат.Успех, "открывать нечем").ЭтоЛожь(); + Ожидаем.Что(Результат.Сообщение, "названа команда").Содержит("open"); + +КонецПроцедуры + +Процедура ТестДолжен_ОтказатьсяОткрыватьПустойАдрес() Экспорт + + Запускатель.Установить("open"); + + Результат = Новый ОткрывательБраузера(Запускатель, "macos").Открыть(""); + + Ожидаем.Что(Результат.Успех, "открывать нечего").ЭтоЛожь(); + Ожидаем.Что(Запускатель.Запуски.Количество(), "запусков не было").Равно(0); + +КонецПроцедуры + +Процедура ТестДолжен_ОпределитьСемействоТекущейПлатформы() Экспорт + + Семейство = Новый ОткрывательБраузера(Запускатель).Семейство(); + + Ожидаем.Что(Семейство = "windows" ИЛИ Семейство = "macos" ИЛИ Семейство = "unix", + СтрШаблон("семейство платформы определено как <%1>", Семейство)).ЭтоИстина(); + +КонецПроцедуры + +//////////////////////////////////////////////////////////////////////////////// +// Адрес приходит от хаба и попадает в командную строку + +// Открываем только http(s): file:// или javascript: браузеру давать нельзя. +Процедура ТестДолжен_ОтвергнутьАдресНеПоHttp() Экспорт + + Открыватель = Новый ОткрывательБраузера(Запускатель, "macos"); + Запускатель.Установить("open"); + + Результат = Открыватель.Открыть("javascript:alert(1)"); + + Ожидаем.Что(Результат.Успех, "адрес не по http(s)").ЭтоЛожь(); + Ожидаем.Что(Результат.Сообщение, "причина названа").Содержит("не по http(s)"); + Ожидаем.Что(Запускатель.Запуски.Количество(), "запусков не было").Равно(0); + +КонецПроцедуры + +// Адрес приводится к URL-кодировке до попадания в запуск: пробел и «>» из враждебного +// адреса кодируются и перенаправлением для cmd на Windows не станут. +Процедура ТестДолжен_ЗакодироватьАдресПередЗапуском() Экспорт + + Запускатель.Установить("open"); + Адрес = "http://localhost:3380/cli/login?c=X;touch метка>файл"; + + Открыватель = Новый ОткрывательБраузера(Запускатель, "macos"); + Открыватель.Открыть(Адрес); + + УшедшийАдрес = Параметры(Запускатель.Запуски[0]); + Ожидаем.Что(СтрНайти(УшедшийАдрес, " метка"), "пробел закодирован").Равно(0); + Ожидаем.Что(СтрНайти(УшедшийАдрес, ">файл"), "перенаправление закодировано").Равно(0); + +КонецПроцедуры + +// Адрес приходит от хаба, и в запуске он занимает ровно одно место — отдельный параметр. +// Пока он был куском строки запуска, эту строку разбирала ещё и оболочка: 1commands +// по умолчанию заворачивает запуск в sh -c '...', а кавычку и «$(...)» URL-кодировка +// не трогает, и такой адрес исполнял бы команды. +Процедура ТестДолжен_ОтдатьАдресОтдельнымПараметром() Экспорт + + Запускатель.Установить("open"); + Адрес = "http://localhost:3380/cli/login?c=X&y=1;" + Символ(39) + "$(touch метка)" + Символ(39); + + Открыватель = Новый ОткрывательБраузера(Запускатель, "macos"); + Открыватель.Открыть(Адрес); + + Запуск = Запускатель.Запуски[0]; + + Ожидаем.Что(Запуск.Команда, "запускается само средство, а не оболочка").Равно("open"); + Ожидаем.Что(Запуск.Параметры.Количество(), "адрес — единственный параметр").Равно(1); + Ожидаем.Что(СтрНайти(Запуск.Параметры[0], " "), "пробела в параметре нет").Равно(0); + +КонецПроцедуры + +// Законное процентное кодирование ломать нельзя, а URL-кодировка идемпотентна: +// им кодируется и кириллица в пути, и повторно она её не трогает. +Процедура ТестДолжен_НеТронутьУжеКодированныйАдрес() Экспорт + + Запускатель.Установить("open"); + Адрес = "http://localhost:3380/%D0%BF%D0%B0%D0%BA%D0%B5%D1%82?next=%2Fpackages%3Fpage%3D2"; + + Результат = Новый ОткрывательБраузера(Запускатель, "macos").Открыть(Адрес); + + Ожидаем.Что(Результат.Успех, "адрес открыт").ЭтоИстина(); + Ожидаем.Что(Параметры(Запускатель.Запуски[0]), "адрес ушёл без повторного кодирования") + .Равно(Адрес); + +КонецПроцедуры + +Процедура ТестДолжен_ПрочитатьПеременнуюСредыИзПодставленногоНабора() Экспорт + + Окружение = Новый ОкружениеПроцесса(НаборПеременных("GITHUB_ACTIONS", " true ")); + + Ожидаем.Что(Окружение.Значение("GITHUB_ACTIONS"), "значение без краевых пробелов").Равно("true"); + Ожидаем.Что(Окружение.Задана("GITHUB_ACTIONS"), "переменная задана").ЭтоИстина(); + Ожидаем.Что(Окружение.Значение("НЕТ_ТАКОЙ"), "отсутствующая переменная").Равно(""); + Ожидаем.Что(Окружение.Задана("НЕТ_ТАКОЙ"), "отсутствующая переменная не задана").ЭтоЛожь(); + +КонецПроцедуры + +//////////////////////////////////////////////////////////////////////////////// +// Приглашение opm login + +Процедура ТестДолжен_ПоказатьАдресСКодомДляПерехода() Экспорт + + Вход = Новый ВходВХаб(АдресаХаба.Разобрать("localhost:3380")); + + Ожидаем.Что(Вход.АдресПодтвержденияДляЧеловека(Заявка(АдресПробы(), АдресСКодом())), + "человеку показывается адрес с кодом").Равно(АдресСКодом()); + +КонецПроцедуры + +Процедура ТестДолжен_ОткатитьсяНаАдресБезКода() Экспорт + + Вход = Новый ВходВХаб(АдресаХаба.Разобрать("localhost:3380")); + + Ожидаем.Что(Вход.АдресПодтвержденияДляЧеловека(Заявка(АдресПробы(), "")), + "хаб не дал полной ссылки").Равно(АдресПробы()); + +КонецПроцедуры + +Процедура ТестДолжен_НапечататьПолнуюСсылкуИКодОтдельнойСтрокой() Экспорт + + Строки = Новый КомандаOpm_Login().СтрокиПриглашения(АдресСКодом(), "WDJB-MJHT"); + + Ожидаем.Что(Строки.Количество(), "число строк приглашения").Равно(2); + Ожидаем.Что(Строки[0], "первой печатается полная ссылка").Содержит(АдресСКодом()); + Ожидаем.Что(Строки[1], "код остаётся отдельной строкой").Содержит("WDJB-MJHT"); + +КонецПроцедуры + +//////////////////////////////////////////////////////////////////////////////// + +Функция АдресПробы() + Возврат "http://localhost:3380/cli/login"; +КонецФункции + +// Параметры запуска одной строкой — так их удобнее сверять и читать в отказе теста. +Функция Параметры(Знач Запуск) + Возврат СтрСоединить(Запуск.Параметры, " "); +КонецФункции + +Функция АдресСКодом() + Возврат АдресПробы() + "?user_code=WDJB-MJHT"; +КонецФункции + +Функция Заявка(Знач Адрес, Знач АдресСКодом) + Возврат Новый Структура("АдресПодтверждения, АдресПодтвержденияСКодом", Адрес, АдресСКодом); +КонецФункции + +Функция НаборПеременных(Знач Имя, Знач Значение) + Набор = Новый Соответствие(); + Набор.Вставить(Имя, Значение); + Возврат Набор; +КонецФункции + +Функция ЗагрузитьЗаглушку(Знач ИмяФайла) + Возврат ЗагрузитьСценарий(ОбъединитьПути(ТекущийСценарий().Каталог, "fixtures", ИмяФайла)); +КонецФункции diff --git a/tests/network-errors-test.os b/tests/network-errors-test.os new file mode 100644 index 0000000..731dab2 --- /dev/null +++ b/tests/network-errors-test.os @@ -0,0 +1,124 @@ +#Использовать asserts +#Использовать "../src/core" + +Перем юТест; + +Функция ПолучитьСписокТестов(Знач Тестирование) Экспорт + + юТест = Тестирование; + + СписокТестов = Новый Массив; + + СписокТестов.Добавить("ТестДолжен_УзнатьНеразрешимоеИмяХоста"); + СписокТестов.Добавить("ТестДолжен_УзнатьОтказВСоединении"); + СписокТестов.Добавить("ТестДолжен_УзнатьТаймаут"); + СписокТестов.Добавить("ТестДолжен_УзнатьОшибкуTLS"); + СписокТестов.Добавить("ТестДолжен_ВернутьСутьНеузнаннойОшибки"); + СписокТестов.Добавить("ТестДолжен_СнятьСлужебнуюОбёрткуПлатформы"); + СписокТестов.Добавить("ТестДолжен_НеТрогатьТекстБезОбёртки"); + СписокТестов.Добавить("ТестДолжен_НеЛоматьТелоОтветаХаба"); + СписокТестов.Добавить("ТестДолжен_СобратьСообщениеОНеудачномПодключении"); + + Возврат СписокТестов; + +КонецФункции + +Процедура ПередЗапускомТеста() Экспорт +КонецПроцедуры + +Процедура ПослеЗапускаТеста() Экспорт +КонецПроцедуры + +Процедура ТестДолжен_УзнатьНеразрешимоеИмяХоста() Экспорт + + Ожидаем.Что(ОшибкиСети.Причина(ОбёрнутьКакПлатформа( + "Внешнее исключение (System.Net.WebException): nodename nor servname provided," + + " or not known (lacalhost:3380)")), "macOS").Равно("имя хоста не разрешается"); + + Ожидаем.Что(ОшибкиСети.Причина("The remote name could not be resolved: No such host is known"), "Windows") + .Равно("имя хоста не разрешается"); + + Ожидаем.Что(ОшибкиСети.Причина("Name or service not known"), "Linux") + .Равно("имя хоста не разрешается"); + +КонецПроцедуры + +Процедура ТестДолжен_УзнатьОтказВСоединении() Экспорт + + Ожидаем.Что(ОшибкиСети.Причина(ОбёрнутьКакПлатформа( + "Внешнее исключение (System.Net.WebException): Connection refused [::ffff:127.0.0.1]:9 (127.0.0.1:9)")), + "POSIX").Равно("соединение отклонено — по этому адресу никто не слушает"); + + Ожидаем.Что(ОшибкиСети.Причина("No connection could be made because the target machine actively refused it"), + "Windows").Равно("соединение отклонено — по этому адресу никто не слушает"); + +КонецПроцедуры + +Процедура ТестДолжен_УзнатьТаймаут() Экспорт + + Ожидаем.Что(ОшибкиСети.Причина("The operation has timed out"), "таймаут") + .Равно("истекло время ожидания ответа"); + +КонецПроцедуры + +Процедура ТестДолжен_УзнатьОшибкуTLS() Экспорт + + Ожидаем.Что(ОшибкиСети.Причина("The SSL connection could not be established"), "TLS") + .Равно("не удалось установить защищённое соединение (TLS)"); + + Ожидаем.Что(ОшибкиСети.Причина("The remote certificate is invalid according to the validation procedure"), + "сертификат").Равно("не удалось установить защищённое соединение (TLS)"); + +КонецПроцедуры + +Процедура ТестДолжен_ВернутьСутьНеузнаннойОшибки() Экспорт + + Ожидаем.Что(ОшибкиСети.Причина(ОбёрнутьКакПлатформа("Внешнее исключение (System.Exception): что-то своё")), + "неузнанная причина").Равно("что-то своё"); + + Ожидаем.Что(ОшибкиСети.Причина(""), "пустой текст").Равно("соединение не установлено"); + +КонецПроцедуры + +Процедура ТестДолжен_СнятьСлужебнуюОбёрткуПлатформы() Экспорт + + Обёрнутое = "{Модуль /путь/к/СерверПакетов.os / Error in line: 69 /" + + " Внешнее исключение (System.Net.WebException): Connection refused}"; + + Суть = ОшибкиСети.Суть(Обёрнутое); + + Ожидаем.Что(Суть, "суть без модуля и строки").Равно("Connection refused"); + Ожидаем.Что(СтрНайти(Суть, "Модуль"), "имени модуля быть не должно").Равно(0); + Ожидаем.Что(СтрНайти(Суть, "Error in line"), "номера строки быть не должно").Равно(0); + Ожидаем.Что(СтрНайти(Суть, "System.Net"), "типа исключения быть не должно").Равно(0); + +КонецПроцедуры + +Процедура ТестДолжен_НеТрогатьТекстБезОбёртки() Экспорт + + Ожидаем.Что(ОшибкиСети.Суть("Пакет не найден"), "обычное сообщение").Равно("Пакет не найден"); + +КонецПроцедуры + +// Тело ответа хаба тоже начинается с фигурной скобки — его переносы строк ломать нельзя. +Процедура ТестДолжен_НеЛоматьТелоОтветаХаба() Экспорт + + Тело = "{" + Символы.ПС + """КодСостояние"": 409," + Символы.ПС + """Сообщение"": ""нельзя""" + Символы.ПС + "}"; + + Ожидаем.Что(ОшибкиСети.Суть(Тело), "JSON остаётся как есть").Равно(Тело); + +КонецПроцедуры + +Процедура ТестДолжен_СобратьСообщениеОНеудачномПодключении() Экспорт + + Сообщение = ОшибкиСети.НеУдалосьПодключиться("http://lacalhost:3380", + ОбёрнутьКакПлатформа("Внешнее исключение (System.Net.WebException): nodename nor servname provided")); + + Ожидаем.Что(Сообщение, "готовое сообщение") + .Равно("Не удалось подключиться к http://lacalhost:3380: имя хоста не разрешается"); + +КонецПроцедуры + +Функция ОбёрнутьКакПлатформа(Знач Текст) + Возврат СтрШаблон("{Модуль /путь/к/Модулю.os / Error in line: 42 / %1}", Текст); +КонецФункции diff --git a/tests/package-spec-test.os b/tests/package-spec-test.os new file mode 100644 index 0000000..99c9a4b --- /dev/null +++ b/tests/package-spec-test.os @@ -0,0 +1,661 @@ +#Использовать asserts +#Использовать fs +#Использовать tempfiles +#Использовать "../src/core" + +Перем юТест; +Перем Хаб; +Перем СерверПробы; +Перем КаталогФайловогоЗеркала; + +Функция ПолучитьСписокТестов(Знач Тестирование) Экспорт + + юТест = Тестирование; + + СписокТестов = Новый Массив; + + СписокТестов.Добавить("ТестДолжен_ЗапроситьЛегасиИмяФайлаИСпецификациюОтдельно"); + СписокТестов.Добавить("ТестДолжен_ЗапроситьФайлБезСпецификацииКогдаЕёНеНазвали"); + СписокТестов.Добавить("ТестДолжен_ДонестиВыражениеВерсииБезИскажений"); + СписокТестов.Добавить("ТестДолжен_СобратьАдресЗапросаСоСпецификацией"); + СписокТестов.Добавить("ТестДолжен_СобратьАдресЗапросаБезСпецификации"); + СписокТестов.Добавить("ТестДолжен_ЗакодироватьСпецификациюВАдресеОбратимо"); + СписокТестов.Добавить("ТестДолжен_ОбъяснитьОтказСтарогоХабаНаТег"); + СписокТестов.Добавить("ТестДолжен_ОбъяснитьОтказСтарогоХабаНаВыражениеВерсии"); + СписокТестов.Добавить("ТестДолжен_СохранитьПрежнийОтказНаТочнуюВерсию"); + СписокТестов.Добавить("ТестДолжен_СохранитьПрежнийОтказБезСпецификации"); + СписокТестов.Добавить("ТестДолжен_СверитьКонтрольнуюСуммуРазрешённойВыдачи"); + СписокТестов.Добавить("ТестДолжен_ОтвергнутьРазрешённуюВыдачуСЧужойСуммой"); + СписокТестов.Добавить("ТестДолжен_УдалитьФайлСНесовпавшейСуммой"); + СписокТестов.Добавить("ТестДолжен_ПровестиУстановкуЧерезТуЖеВоронкуЗагрузки"); + СписокТестов.Добавить("ТестДолжен_ОтличитьПустуюСпецификациюОтОтсутствующей"); + СписокТестов.Добавить("ТестДолжен_РазличитьОтказыНомераИВыраженияВерсии"); + СписокТестов.Добавить("ТестДолжен_ОтказатьНаСпецификацииСКосойЧертой"); + СписокТестов.Добавить("ТестДолжен_ОдинаковоОформитьЛюбойОтказУстановки"); + СписокТестов.Добавить("ТестДолжен_ЗапретитьФайловомуЗеркалуПодборПодСпецификацию"); + СписокТестов.Добавить("ТестДолжен_ОставитьФайловомуЗеркалуПодборБезСпецификации"); + + // строку запроса видно только на проводе, а поднять журналирующий хаб нечем, кроме python + Если ЕстьЖурналирующийХаб() Тогда + СписокТестов.Добавить("ТестДолжен_ОтправитьХабуЗапросСПараметромСпецификации"); + СписокТестов.Добавить("ТестДолжен_ДонестиСпецификациюАдреснойФормы"); + КонецЕсли; + + Возврат СписокТестов; + +КонецФункции + +Процедура ПередЗапускомТеста() Экспорт + + Хаб = ЗагрузитьЗаглушку("ЗаглушкаСервераПакетов.os"); + Хаб.ОбъявитьПакет("vrunner"); + Хаб.Отвечать(ОтветХаба(200, "содержимое пакета vrunner")); + + СерверПробы = Новый СерверПакетов("ХабПробы", "http://hub.example.com", "/download/", "/push", 80); + КаталогФайловогоЗеркала = ""; + + НастройкиOpm.СброситьНастройки(); + НастройкиOpm.ДобавитьСерверПакетов(Хаб); + РаботаСПакетами.СброситьМенеджерПолученияПакетов(); + +КонецПроцедуры + +Процедура ПослеЗапускаТеста() Экспорт + + НастройкиOpm.СброситьНастройки(); + РаботаСПакетами.СброситьМенеджерПолученияПакетов(); + ВременныеФайлы.Удалить(); + +КонецПроцедуры + +//////////////////////////////////////////////////////////////////////////////// +// Форма запроса: имя файла легаси, спецификация — отдельным параметром + +// Имя артефакта остаётся прежним нарочно: на старом хабе такого файла нет, и он честно +// отвечает 404 вместо того, чтобы молча отдать последнюю версию. +Процедура ТестДолжен_ЗапроситьЛегасиИмяФайлаИСпецификациюОтдельно() Экспорт + + Скачать("vrunner", "stable"); + + Ожидаем.Что(Хаб.ПоследнийЗапрос().ИмяРесурса, "имя ресурса").Равно("vrunner/vrunner-stable.ospx"); + Ожидаем.Что(Хаб.ПоследнийЗапрос().Спецификация, "спецификация").Равно("stable"); + +КонецПроцедуры + +Процедура ТестДолжен_ЗапроситьФайлБезСпецификацииКогдаЕёНеНазвали() Экспорт + + Скачать("vrunner", Неопределено); + + Ожидаем.Что(Хаб.ПоследнийЗапрос().ИмяРесурса, "имя ресурса").Равно("vrunner/vrunner.ospx"); + Ожидаем.Что(Хаб.ПоследнийЗапрос().Спецификация, "спецификация не названа").Равно(Неопределено); + +КонецПроцедуры + +Процедура ТестДолжен_ДонестиВыражениеВерсииБезИскажений() Экспорт + + ВыраженияВерсий = Новый Массив; + ВыраженияВерсий.Добавить("^1.4"); + ВыраженияВерсий.Добавить("~1.4"); + ВыраженияВерсий.Добавить("1.4"); + + Для Каждого Выражение Из ВыраженияВерсий Цикл + + Скачать("vrunner", Выражение); + + Ожидаем.Что(Хаб.ПоследнийЗапрос().Спецификация, "спецификация " + Выражение).Равно(Выражение); + Ожидаем.Что(Хаб.ПоследнийЗапрос().ИмяРесурса, "имя ресурса для " + Выражение) + .Равно(СтрШаблон("vrunner/vrunner-%1.ospx", Выражение)); + + КонецЦикла; + +КонецПроцедуры + +//////////////////////////////////////////////////////////////////////////////// +// Адрес запроса к хабу + +Процедура ТестДолжен_СобратьАдресЗапросаСоСпецификацией() Экспорт + + Ожидаем.Что(СерверПробы.АдресРесурса("vrunner/vrunner-stable.ospx", "stable"), "адрес с тегом") + .Равно("/download/vrunner/vrunner-stable.ospx?spec=stable"); + + Ожидаем.Что(СерверПробы.АдресРесурса("vrunner/vrunner-1.0.0.ospx", "1.0.0"), "адрес с точной версией") + .Равно("/download/vrunner/vrunner-1.0.0.ospx?spec=1.0.0"); + +КонецПроцедуры + +Процедура ТестДолжен_СобратьАдресЗапросаБезСпецификации() Экспорт + + Ожидаем.Что(СерверПробы.АдресРесурса("vrunner/vrunner.ospx"), "адрес без спецификации") + .Равно("/download/vrunner/vrunner.ospx"); + + Ожидаем.Что(СерверПробы.АдресРесурса("list.txt"), "список пакетов").Равно("/download/list.txt"); + +КонецПроцедуры + +// Косая черта внутри значения обязана уехать закодированной, иначе она разрежет путь. +Процедура ТестДолжен_ЗакодироватьСпецификациюВАдресеОбратимо() Экспорт + + Образцы = Новый Массив; + Образцы.Добавить("^1.4"); + Образцы.Добавить("~1.4"); + Образцы.Добавить("1.0.0-rc.1+build"); + Образцы.Добавить(">=1.0 <2.0"); + + Для Каждого Образец Из Образцы Цикл + + Адрес = СерверПробы.АдресРесурса("vrunner/vrunner.ospx", Образец); + Параметр = Сред(Адрес, СтрНайти(Адрес, "?spec=") + СтрДлина("?spec=")); + + Ожидаем.Что(СтрНайти(Параметр, "^"), "^ закодирован в " + Адрес).Равно(0); + Ожидаем.Что(СтрНайти(Параметр, "+"), "+ закодирован в " + Адрес).Равно(0); + Ожидаем.Что(СтрНайти(Параметр, " "), "пробел закодирован в " + Адрес).Равно(0); + Ожидаем.Что(РаскодироватьСтроку(Параметр, СпособКодированияСтроки.КодировкаURL), + "значение восстанавливается: " + Адрес).Равно(Образец); + + КонецЦикла; + + Ожидаем.Что(СерверПробы.АдресРесурса("vrunner/vrunner-^1.4.ospx", "^1.4"), "^ закодирован и в имени файла") + .Равно("/download/vrunner/vrunner-%5E1.4.ospx?spec=%5E1.4"); + +КонецПроцедуры + +//////////////////////////////////////////////////////////////////////////////// +// Отказ старого хаба объясняется по существу + +Процедура ТестДолжен_ОбъяснитьОтказСтарогоХабаНаТег() Экспорт + + Хаб.Отвечать(ОтветХаба(404, "")); + + Отказ = ТекстОтказа("vrunner", "stable"); + + Ожидаем.Что(Отказ, "отказ называет спецификацию").Содержит(""); + Ожидаем.Что(Отказ, "отказ называет параметр хаба").Содержит("spec"); + Ожидаем.Что(СтрНайти(Отказ, "в хабе не найден"), "ложного следа про файл нет").Равно(0); + +КонецПроцедуры + +Процедура ТестДолжен_ОбъяснитьОтказСтарогоХабаНаВыражениеВерсии() Экспорт + + Хаб.Отвечать(ОтветХаба(404, "")); + + Отказ = ТекстОтказа("vrunner", "^1.4"); + + Ожидаем.Что(Отказ, "отказ называет спецификацию").Содержит("<^1.4>"); + Ожидаем.Что(СтрНайти(Отказ, "в хабе не найден"), "ложного следа про файл нет").Равно(0); + +КонецПроцедуры + +Процедура ТестДолжен_СохранитьПрежнийОтказНаТочнуюВерсию() Экспорт + + Хаб.Отвечать(ОтветХаба(404, "")); + + Ожидаем.Что(ТекстОтказа("vrunner", "1.0.0"), "прежний текст отказа") + .Содержит("файл vrunner-1.0.0.ospx в хабе не найден"); + +КонецПроцедуры + +Процедура ТестДолжен_СохранитьПрежнийОтказБезСпецификации() Экспорт + + Хаб.Отвечать(ОтветХаба(404, "")); + + Ожидаем.Что(ТекстОтказа("vrunner", Неопределено), "прежний текст отказа") + .Содержит("файл vrunner.ospx в хабе не найден"); + +КонецПроцедуры + +//////////////////////////////////////////////////////////////////////////////// +// Контрольная сумма сверяется и на разрешённой по спецификации выдаче + +Процедура ТестДолжен_СверитьКонтрольнуюСуммуРазрешённойВыдачи() Экспорт + + Тело = "содержимое пакета vrunner"; + Ответ = ОтветХаба(200, Тело); + Ответ.ОбъявитьКонтрольнуюСумму(СуммаТела(Тело)); + Хаб.Отвечать(Ответ); + + ПутьКПакету = Скачать("vrunner", "stable"); + + Ожидаем.Что(ФС.ФайлСуществует(ПутьКПакету), "пакет скачан").ЭтоИстина(); + +КонецПроцедуры + +Процедура ТестДолжен_ОтвергнутьРазрешённуюВыдачуСЧужойСуммой() Экспорт + + Ответ = ОтветХаба(200, "содержимое пакета vrunner"); + Ответ.ОбъявитьКонтрольнуюСумму("0000000000000000000000000000000000000000000000000000000000000000"); + Хаб.Отвечать(Ответ); + + Ожидаем.Что(ТекстОтказа("vrunner", "stable"), "сумма не совпала") + .Содержит("контрольная сумма не совпала"); + +КонецПроцедуры + +// Путь загрузки задаёт вызывающий: у opm download это каталог рядом с настоящими +// пакетами, и непроверенный файл, оставшийся там, — ровно то, от чего сверка защищает. +Процедура ТестДолжен_УдалитьФайлСНесовпавшейСуммой() Экспорт + + Ответ = ОтветХаба(200, "содержимое пакета vrunner"); + Ответ.ОбъявитьКонтрольнуюСумму("0000000000000000000000000000000000000000000000000000000000000000"); + Хаб.Отвечать(Ответ); + + ПутьЗагрузки = ВременныеФайлы.НовоеИмяФайла("ospx"); + + Попытка + Менеджер = Новый МенеджерПолученияПакетов(); + Менеджер.ПолучитьПакет("vrunner", "stable", ПутьЗагрузки); + Исключение + Ожидаем.Что(ФС.ФайлСуществует(ПутьЗагрузки), "непроверенный файл на диске не остался").ЭтоЛожь(); + Возврат; + КонецПопытки; + + юТест.ПрерватьТест("Ожидали отказ: контрольная сумма не совпала"); + +КонецПроцедуры + +//////////////////////////////////////////////////////////////////////////////// +// Установка, обновление и зависимости идут одной воронкой загрузки + +// МенеджерУстановкиПакетов — общий вход установки, обновления, скачивания и разрешения +// зависимостей: доказываем, что до хаба он доходит тем же запросом. +Процедура ТестДолжен_ПровестиУстановкуЧерезТуЖеВоронкуЗагрузки() Экспорт + + КаталогУстановки = юТест.ИмяВременногоФайла(); + Установка = Новый МенеджерУстановкиПакетов(РежимУстановкиПакетов.Скачивание, КаталогУстановки); + + Попытка + // тело заглушки архивом не является: до распаковки дело дойдёт, а нам важен запрос + Установка.УстановитьПакетПоИмениИВерсии("vrunner", "stable"); + Исключение + КонецПопытки; + + Ожидаем.Что(Хаб.ПоследнийЗапрос().ИмяРесурса, "имя ресурса").Равно("vrunner/vrunner-stable.ospx"); + Ожидаем.Что(Хаб.ПоследнийЗапрос().Спецификация, "спецификация").Равно("stable"); + +КонецПроцедуры + +//////////////////////////////////////////////////////////////////////////////// +// Отсутствующая, пустая и невыразимая спецификация + +// «pkg» отдаёт выбор версии хабу, «pkg@» назвал спецификацию и оставил пустой. +// Схлопнуть их — значит на опечатку тихо поставить последнюю версию. +Процедура ТестДолжен_ОтличитьПустуюСпецификациюОтОтсутствующей() Экспорт + + Скачать("vrunner", ""); + + Ожидаем.Что(Хаб.ПоследнийЗапрос().ИмяРесурса, "имя ресурса пустой спецификации") + .Равно("vrunner/vrunner-.ospx"); + + Хаб.Отвечать(ОтветХаба(404, "")); + + Ожидаем.Что(ТекстОтказа("vrunner", ""), "отказ пустой спецификации") + .Содержит("файл vrunner-.ospx в хабе не найден"); + +КонецПроцедуры + +// Номер версии на 404 остаётся «файл не найден», а не-номер объясняется через параметр spec. +Процедура ТестДолжен_РазличитьОтказыНомераИВыраженияВерсии() Экспорт + + Хаб.Отвечать(ОтветХаба(404, "")); + + Ожидаем.Что(ТекстОтказа("vrunner", "1.4"), "короткий номер — файл не найден") + .Содержит("файл vrunner-1.4.ospx в хабе не найден"); + + Ожидаем.Что(ТекстОтказа("vrunner", "1.x"), "маска версии — не номер вовсе") + .Содержит("хаб не умеет разрешать спецификацию <1.x>"); + +КонецПроцедуры + +// Косая черта разрезала бы путь запроса: хаб получил бы имя пакета, которого не спрашивали. +Процедура ТестДолжен_ОтказатьНаСпецификацииСКосойЧертой() Экспорт + + Ожидаем.Что(ТекстОтказа("vrunner", "feature/foo"), "отказ до запроса") + .Содержит("содержит косую черту"); + + Ожидаем.Что(Хаб.Запросы.Количество(), "до хаба запрос не дошёл").Равно(0); + +КонецПроцедуры + +// Все три отказа установки собираются одним фасадом: пользователь читает их подряд +// в одном журнале, и разнобой в оформлении читался бы разными ошибками. +Процедура ТестДолжен_ОдинаковоОформитьЛюбойОтказУстановки() Экспорт + + Ожидаем.Что(ТекстОтказа("нетутакого", Неопределено), "пакета нет в хабе") + .Содержит("Ошибка установки пакета нетутакого: Пакет не найден"); + + Ожидаем.Что(ТекстОтказа("vrunner", "feature/foo"), "спецификация невыразима") + .Содержит("Ошибка установки пакета vrunner: спецификация"); + + Хаб.Отвечать(ОтветХаба(404, "")); + + Ожидаем.Что(ТекстОтказа("vrunner", "stable"), "хаб ответил 404") + .Содержит("Ошибка установки пакета vrunner: хаб не умеет разрешать"); + +КонецПроцедуры + +//////////////////////////////////////////////////////////////////////////////// +// Файловое зеркало не подбирает версию под названную спецификацию + +// Подбор старшей версии каталога — это и есть молчаливая подмена: спецификацию +// каталог разрешить не может, значит запрошенного файла просто нет. +Процедура ТестДолжен_ЗапретитьФайловомуЗеркалуПодборПодСпецификацию() Экспорт + + Зеркало = Новый ФайловыйСерверПакетов("зеркалоПробы", "file://", КаталогЗеркала()); + + Ответ = Зеркало.ПолучитьРесурс("vrunner/vrunner-stable.ospx", "stable"); + Ожидаем.Что(Ответ.КодСостояния, "тег").Равно(404); + + Ответ = Зеркало.ПолучитьРесурс("vrunner/vrunner-0.0.1.ospx", "0.0.1"); + Ожидаем.Что(Ответ.КодСостояния, "версия, которой в зеркале нет").Равно(404); + + Ответ = Зеркало.ПолучитьРесурс("vrunner/vrunner-9.9.9.ospx", "9.9.9"); + Ожидаем.Что(Ответ.КодСостояния, "версия, которая в зеркале есть").Равно(200); + +КонецПроцедуры + +// Без спецификации подбор — прежнее и единственное поведение файлового зеркала. +Процедура ТестДолжен_ОставитьФайловомуЗеркалуПодборБезСпецификации() Экспорт + + Зеркало = Новый ФайловыйСерверПакетов("зеркалоПробы", "file://", КаталогЗеркала()); + + Ответ = Зеркало.ПолучитьРесурс("vrunner/vrunner.ospx"); + + Ожидаем.Что(Ответ.КодСостояния, "подбор старшей версии").Равно(200); + +КонецПроцедуры + +//////////////////////////////////////////////////////////////////////////////// +// Строка запроса на проводе + +// Единственное место, где видно ровно то, что уходит хабу: имя файла легаси-формы +// и спецификация отдельным параметром. +// +// Плюс выбран нарочно. Платформа сама чинит в адресе "^" и пробел, поэтому на них +// собственное кодирование клиента не проверяется — сломай его, и строка запроса +// не изменится. Плюс платформа оставляет как есть, а в строке запроса он читается +// пробелом: тут кодирование либо есть, либо спецификация приезжает искажённой. +Процедура ТестДолжен_ОтправитьХабуЗапросСПараметромСпецификации() Экспорт + + Стенд = ПоднятьЖурналирующийХаб(); + + Попытка + Живой = Новый СерверПакетов("ЖивойХабПробы", "http://127.0.0.1", "/download/", "/push", Стенд.Порт); + ЗапроситьИЗакрыть(Живой, "vrunner/vrunner-stable.ospx", "stable"); + ЗапроситьИЗакрыть(Живой, "vrunner/vrunner-1.0.0+build.ospx", "1.0.0+build"); + ЗапроситьИЗакрыть(Живой, "vrunner/vrunner.ospx", Неопределено); + Исключение + ОстановитьЖурналирующийХаб(Стенд); + ВызватьИсключение; + КонецПопытки; + + ОстановитьЖурналирующийХаб(Стенд); + + Запросы = ЖурналЗапросов(Стенд); + + Ожидаем.Что(Запросы.Количество(), "хаб принял три запроса").Равно(3); + Ожидаем.Что(Запросы[0], "спецификация-тег") + .Равно("GET /download/vrunner/vrunner-stable.ospx?spec=stable"); + Ожидаем.Что(Запросы[1], "плюс закодирован клиентом, а не платформой") + .Равно("GET /download/vrunner/vrunner-1.0.0%2Bbuild.ospx?spec=1.0.0%2Bbuild"); + Ожидаем.Что(Запросы[2], "без спецификации — без параметра") + .Равно("GET /download/vrunner/vrunner.ospx"); + +КонецПроцедуры + +// Адресная форма команды обязана упираться в тот же инвариант, что и классическая: +// хаб не может ответить 200 на спецификацию, которую не разобрал. +Процедура ТестДолжен_ДонестиСпецификациюАдреснойФормы() Экспорт + + Стенд = ПоднятьЖурналирующийХаб(); + КаталогЗагрузки = юТест.ИмяВременногоФайла(); + + Попытка + СкачатьПоАдресу(СтрШаблон("127.0.0.1:%1/vrunner@stable", Стенд.Порт), КаталогЗагрузки); + СкачатьПоАдресу(СтрШаблон("127.0.0.1:%1/corp/vrunner@stable", Стенд.Порт), КаталогЗагрузки); + СкачатьПоАдресу(СтрШаблон("127.0.0.1:%1/corp/vrunner@^1.4", Стенд.Порт), КаталогЗагрузки); + Исключение + ОстановитьЖурналирующийХаб(Стенд); + ВызватьИсключение; + КонецПопытки; + + ОстановитьЖурналирующийХаб(Стенд); + + Запросы = ЗапросыЗаПакетом(ЖурналЗапросов(Стенд)); + + Ожидаем.Что(Запросы.Количество(), "хаб принял три запроса за пакетом").Равно(3); + Ожидаем.Что(Запросы[0], "адрес без пула: легаси-путь и спецификация параметром") + .Равно("GET /download/vrunner/vrunner-stable.ospx?spec=stable"); + Ожидаем.Что(Запросы[1], "адрес с пулом: путь пула и та же спецификация") + .Равно("GET /api/v1/pools/corp/download/vrunner/vrunner-stable.ospx?spec=stable"); + Ожидаем.Что(Запросы[2], "выражение версии уходит тем же путём") + .Равно("GET /api/v1/pools/corp/download/vrunner/vrunner-%5E1.4.ospx?spec=%5E1.4"); + +КонецПроцедуры + +//////////////////////////////////////////////////////////////////////////////// + +Функция ЗагрузитьЗаглушку(Знач ИмяФайла) + Возврат ЗагрузитьСценарий(ОбъединитьПути(ТекущийСценарий().Каталог, "fixtures", ИмяФайла)); +КонецФункции + +// Каталог-зеркало с единственной старшей версией: подбор именно её выдал бы за +// запрошенную любую другую. +Функция КаталогЗеркала() + + Если ЗначениеЗаполнено(КаталогФайловогоЗеркала) Тогда + Возврат КаталогФайловогоЗеркала; + КонецЕсли; + + КаталогФайловогоЗеркала = юТест.ИмяВременногоФайла(); + КаталогПакета = ОбъединитьПути(КаталогФайловогоЗеркала, "vrunner"); + СоздатьКаталог(КаталогПакета); + + Запись = Новый ЗаписьТекста(ОбъединитьПути(КаталогПакета, "vrunner-9.9.9.ospx")); + Запись.Записать("не архив, но файл на месте"); + Запись.Закрыть(); + + Возврат КаталогФайловогоЗеркала; + +КонецФункции + +Функция ОтветХаба(Знач Код, Знач Тело) + Ответ = ЗагрузитьЗаглушку("ЗаглушкаОтветаХаба.os"); + Ответ.Отвечать(Код, Тело); + Возврат Ответ; +КонецФункции + +Функция Скачать(Знач ИмяПакета, Знач Спецификация) + Менеджер = Новый МенеджерПолученияПакетов(); + Возврат Менеджер.ПолучитьПакет(ИмяПакета, Спецификация); +КонецФункции + +Функция СуммаТела(Знач Тело) + ФайлЭталона = ВременныеФайлы.НовоеИмяФайла("bin"); + ДвоичныеДанные = ПолучитьДвоичныеДанныеИзСтроки(Тело); + ДвоичныеДанные.Записать(ФайлЭталона); + Возврат КонтрольныеСуммы.СуммаФайла(ФайлЭталона); +КонецФункции + +/////////////////////////////////////////////////////////////////// +// Журналирующий хаб: пишет строку запроса в файл и отдаёт заглушку пакета + +Функция ЕстьЖурналирующийХаб() + + СИ = Новый СистемнаяИнформация(); + Если Найти(СИ.ВерсияОС, "Windows") > 0 Тогда + Возврат Ложь; + КонецЕсли; + + Проба = СоздатьПроцесс("python3 --version", , Истина); + Попытка + Проба.Запустить(); + Проба.ОжидатьЗавершения(); + Возврат Проба.КодВозврата = 0; + Исключение + Возврат Ложь; + КонецПопытки; + +КонецФункции + +Функция ПоднятьЖурналирующийХаб() + + КаталогСтенда = юТест.ИмяВременногоФайла(); + СоздатьКаталог(КаталогСтенда); + + Стенд = Новый Структура("Журнал, ФайлПорта, Порт, Процесс"); + Стенд.Журнал = ОбъединитьПути(КаталогСтенда, "запросы.log"); + Стенд.ФайлПорта = ОбъединитьПути(КаталогСтенда, "порт.txt"); + + Сценарий = ОбъединитьПути(КаталогСтенда, "хаб.py"); + Запись = Новый ЗаписьТекста(Сценарий, КодировкаТекста.UTF8); + Запись.Записать(СценарийХаба()); + Запись.Закрыть(); + + Стенд.Процесс = СоздатьПроцесс(СтрШаблон("python3 ""%1"" ""%2"" ""%3""", + Сценарий, Стенд.Журнал, Стенд.ФайлПорта), , Истина); + Стенд.Процесс.Запустить(); + + Попытка + Стенд.Порт = ДождатьсяПорта(Стенд.ФайлПорта); + Исключение + ОстановитьЖурналирующийХаб(Стенд); + ВызватьИсключение; + КонецПопытки; + + Возврат Стенд; + +КонецФункции + +Функция ДождатьсяПорта(Знач ФайлПорта) + + Для Попытка_ = 1 По 100 Цикл + + Если ФС.ФайлСуществует(ФайлПорта) Тогда + Чтение = Новый ЧтениеТекста(ФайлПорта); + Значение = СокрЛП(Строка(Чтение.Прочитать())); + Чтение.Закрыть(); + Если ЗначениеЗаполнено(Значение) Тогда + Возврат Число(Значение); + КонецЕсли; + КонецЕсли; + + Приостановить(100); + + КонецЦикла; + + ВызватьИсключение "Журналирующий хаб не сообщил порт"; + +КонецФункции + +Процедура ОстановитьЖурналирующийХаб(Знач Стенд) + + Попытка + Стенд.Процесс.Завершить(); + Исключение + КонецПопытки; + +КонецПроцедуры + +Функция ЖурналЗапросов(Знач Стенд) + + Результат = Новый Массив(); + + Если НЕ ФС.ФайлСуществует(Стенд.Журнал) Тогда + Возврат Результат; + КонецЕсли; + + Чтение = Новый ЧтениеТекста(Стенд.Журнал); + Содержимое = Чтение.Прочитать(); + Чтение.Закрыть(); + + Для Каждого Строка_ Из СтрРазделить(Строка(Содержимое), Символы.ПС, Ложь) Цикл + Если НЕ ПустаяСтрока(СокрЛП(Строка_)) Тогда + Результат.Добавить(СокрЛП(Строка_)); + КонецЕсли; + КонецЦикла; + + Возврат Результат; + +КонецФункции + +// Запросы за списком пакетов к делу не относятся: их клиент делает и без спецификации. +Функция ЗапросыЗаПакетом(Знач Журнал) + + Результат = Новый Массив(); + + Для Каждого Запрос Из Журнал Цикл + Если СтрНайти(Запрос, "list.txt") = 0 Тогда + Результат.Добавить(Запрос); + КонецЕсли; + КонецЦикла; + + Возврат Результат; + +КонецФункции + +// Скачивание по адресу хаба из командной строки. Тело заглушки архивом не является: +// до распаковки дело дойдёт, а нам важна строка запроса. +Процедура СкачатьПоАдресу(Знач Адрес, Знач КаталогЗагрузки) + + Попытка + РаботаСПакетами.УстановитьПакетПоАдресу(Адрес, РежимУстановкиПакетов.Скачивание, КаталогЗагрузки); + Исключение + КонецПопытки; + + НастройкиOpm.СброситьНастройки(); + РаботаСПакетами.СброситьМенеджерПолученияПакетов(); + +КонецПроцедуры + +Процедура ЗапроситьИЗакрыть(Знач Сервер, Знач ИмяРесурса, Знач Спецификация) + + Ответ = Сервер.ПолучитьРесурс(ИмяРесурса, Спецификация); + + Если Ответ = Неопределено Тогда + ВызватьИсключение СтрШаблон("Журналирующий хаб не ответил на %1", ИмяРесурса); + КонецЕсли; + + Ответ.Закрыть(); + +КонецПроцедуры + +Функция СценарийХаба() + + Возврат "import http.server, socketserver, sys + | + |журнал, файл_порта = sys.argv[1], sys.argv[2] + | + |class Обработчик(http.server.BaseHTTPRequestHandler): + | def do_GET(self): + | with open(журнал, 'a', encoding='utf-8') as f: + | f.write('GET ' + self.path + chr(10)) + | тело = b'vrunner' + bytes([10]) if self.path.endswith('list.txt') else b'OSPX' + | self.send_response(200) + | self.send_header('Content-Length', str(len(тело))) + | self.end_headers() + | self.wfile.write(тело) + | def log_message(self, *args): + | pass + | + |socketserver.TCPServer.allow_reuse_address = True + |сервер = socketserver.TCPServer(('127.0.0.1', 0), Обработчик) + |with open(файл_порта, 'w') as f: + | f.write(str(сервер.server_address[1])) + |сервер.serve_forever() + |"; + +КонецФункции + +/////////////////////////////////////////////////////////////////// + +// Текст, который пользователь увидит вместо установленного пакета. +Функция ТекстОтказа(Знач ИмяПакета, Знач Спецификация) + + Попытка + Скачать(ИмяПакета, Спецификация); + Исключение + Возврат ОписаниеОшибки(); + КонецПопытки; + + ВызватьИсключение СтрШаблон("Установка <%1@%2> должна была провалиться", ИмяПакета, Спецификация); + +КонецФункции diff --git a/tests/push-target-test.os b/tests/push-target-test.os new file mode 100644 index 0000000..3817866 --- /dev/null +++ b/tests/push-target-test.os @@ -0,0 +1,519 @@ +#Использовать asserts +#Использовать cli +#Использовать tempfiles +#Использовать "../src/core" +#Использовать "../src/cmd" + +Перем юТест; +Перем Пуш; + +Функция ПолучитьСписокТестов(Знач Тестирование) Экспорт + + юТест = Тестирование; + + СписокТестов = Новый Массив; + + СписокТестов.Добавить("ТестДолжен_ВзятьЦельИзАдресаОпции"); + СписокТестов.Добавить("ТестДолжен_ОтказатьНаАдресеПакетаВОпцииХаба"); + СписокТестов.Добавить("ТестДолжен_ВзятьЦельИзНастроекБезАдреса"); + СписокТестов.Добавить("ТестДолжен_УзнатьПулВЦелиИзНастроек"); + СписокТестов.Добавить("ТестДолжен_ПропуститьСерверБезМаршрутаПубликации"); + СписокТестов.Добавить("ТестДолжен_ВзятьЦельПоИмениЗеркала"); + СписокТестов.Добавить("ТестДолжен_ОтказатьНаФайловомЗеркалеПоИмени"); + СписокТестов.Добавить("ТестДолжен_ОтличитьЛегасиХабОтОстальных"); + СписокТестов.Добавить("ТестДолжен_ОтклонитьАдресВПозиционномАргументе"); + СписокТестов.Добавить("ТестДолжен_НеПринятьАдресПулаВКанале"); + СписокТестов.Добавить("ТестДолжен_ПубликоватьТокеномКонвейераПоверхТокенаКоманды"); + СписокТестов.Добавить("ТестДолжен_ВзятьТокенКомандыБезДоверия"); + СписокТестов.Добавить("ТестДолжен_ВзятьАвторизациюИзОписанияСервера"); + СписокТестов.Добавить("ТестДолжен_ОтдатьТокенЛегасиХабуЗаголовкомOauthToken"); + СписокТестов.Добавить("ТестДолжен_ОбъяснитьОтсутствиеТокенаПричинойБезДоверия"); + + СписокТестов.Добавить("ТестДолжен_УвидетьПринудительнуюПубликациюВОпцииForce"); + СписокТестов.Добавить("ТестДолжен_СчитатьПубликациюОбычнойБезОпцииForce"); + СписокТестов.Добавить("ТестДолжен_ОтказатьНаНепонятномЗначенииОпцииForce"); + СписокТестов.Добавить("ТестДолжен_ДописатьПараметрForceКМаршрутуПубликации"); + СписокТестов.Добавить("ТестДолжен_ОставитьМаршрутПубликацииБезПараметровБезОпцииForce"); + СписокТестов.Добавить("ТестДолжен_ДописатьForceКМаршрутуСоСтрокойЗапроса"); + + // строку запроса видно только на проводе, а поднять журналирующий хаб нечем, кроме python + Если ЖурналирующийХаб().Доступен() Тогда + СписокТестов.Добавить("ТестДолжен_ОтправитьХабуПушСПараметромForce"); + СписокТестов.Добавить("ТестДолжен_ДонестиОпциюForceОтКоманднойСтрокиДоЗапроса"); + КонецЕсли; + + Возврат СписокТестов; + +КонецФункции + +Процедура ПередЗапускомТеста() Экспорт + + Пуш = Новый КомандаOpm_Push(); + НастройкиOpm.СброситьНастройки(); + +КонецПроцедуры + +Процедура ПослеЗапускаТеста() Экспорт + + НастройкиOpm.СброситьНастройки(); + ВременныеФайлы.Удалить(); + +КонецПроцедуры + +//////////////////////////////////////////////////////////////////////////////// +// Куда публикуем + +Процедура ТестДолжен_ВзятьЦельИзАдресаОпции() Экспорт + + Цель = Пуш.ЦельПубликации("localhost:3380/corp"); + + Ожидаем.Что(Цель.Сервер, "адрес сервера").Равно("http://localhost"); + Ожидаем.Что(Цель.Порт, "порт из адреса").Равно(3380); + Ожидаем.Что(Цель.Ресурс, "маршрут публикации пула").Равно("/api/v1/pools/corp/push"); + Ожидаем.Что(Цель.Пул, "имя пула").Равно("corp"); + Ожидаем.Что(Цель.БазовыйАдрес, "аудитория по умолчанию").Равно("http://localhost:3380"); + Ожидаем.Что(Цель.Легаси, "адрес ведёт в OpenHub").ЭтоЛожь(); + +КонецПроцедуры + +Процедура ТестДолжен_ОтказатьНаАдресеПакетаВОпцииХаба() Экспорт + + Попытка + Пуш.ЦельПубликации("localhost:3380/corp/my-package"); + Исключение + Ожидаем.Что(ИнформацияОбОшибке().Описание, "сказано, что ждали адрес хаба") + .Содержит("не пакета"); + Возврат; + КонецПопытки; + + юТест.ПрерватьТест("Ожидали отказ на адресе пакета в <--hub>"); + +КонецПроцедуры + +// Адрес не указан — публикуем туда, куда описан вход в opm.cfg, а не в хаб по умолчанию. +Процедура ТестДолжен_ВзятьЦельИзНастроекБезАдреса() Экспорт + + НастройкиOpm.ДобавитьСерверПакетов(СерверИзНастроек("свой", "http://localhost", 3391, + "/download/", "/push", "Bearer сохранённый")); + + Цель = Пуш.ЦельПубликации(""); + + Ожидаем.Что(Цель.Имя, "сервер из настроек").Равно("свой"); + Ожидаем.Что(Цель.Сервер, "адрес сервера").Равно("http://localhost"); + Ожидаем.Что(Цель.Порт, "порт из настройки").Равно(3391); + Ожидаем.Что(Цель.БазовыйАдрес, "адрес с портом").Равно("http://localhost:3391"); + Ожидаем.Что(Цель.Авторизация, "сохранённый вход").Равно("Bearer сохранённый"); + Ожидаем.Что(Цель.Легаси, "свой хаб легаси-протоколом не говорит").ЭтоЛожь(); + +КонецПроцедуры + +Процедура ТестДолжен_УзнатьПулВЦелиИзНастроек() Экспорт + + НастройкиOpm.ДобавитьСерверПакетов(СерверИзНастроек("пул", "http://localhost", 3380, + "/api/v1/pools/corp/download/", "/api/v1/pools/corp/push", "Bearer сохранённый")); + + Цель = Пуш.ЦельПубликации(""); + + Ожидаем.Что(Цель.Пул, "пул узнан по маршруту публикации").Равно("corp"); + +КонецПроцедуры + +// У запасного зеркала и файлового хранилища маршрута публикации нет: публиковать туда нечем. +Процедура ТестДолжен_ПропуститьСерверБезМаршрутаПубликации() Экспорт + + НастройкиOpm.ДобавитьСерверПакетов(СерверыПакетов.ЗапаснойСервер()); + НастройкиOpm.ДобавитьСерверПакетов(СерверИзНастроек("свой", "http://localhost", 3391, + "/download/", "/push", "")); + + Ожидаем.Что(Пуш.ЦельПубликации("").Имя, "зеркало без публикации пропущено").Равно("свой"); + +КонецПроцедуры + +Процедура ТестДолжен_ВзятьЦельПоИмениЗеркала() Экспорт + + НастройкиOpm.ДобавитьСерверПакетов(СерверИзНастроек("первый", "http://localhost", 3391, + "/download/", "/push", "")); + НастройкиOpm.ДобавитьСерверПакетов(СерверИзНастроек("второй", "http://localhost", 3392, + "/download/", "/push", "")); + + Ожидаем.Что(Пуш.ЦельПубликации("", "второй").Порт, "названное зеркало").Равно(3392); + +КонецПроцедуры + +// У файлового зеркала нет ни маршрута публикации, ни полей таймаута с заголовками: +// названное по имени, оно обязано кончиться объяснением, а не ошибкой обращения к свойству. +Процедура ТестДолжен_ОтказатьНаФайловомЗеркалеПоИмени() Экспорт + + НастройкиOpm.ДобавитьСерверПакетов(Новый ФайловыйСерверПакетов("зеркало", "file://", ТекущийКаталог())); + + Попытка + Пуш.ЦельПубликации("", "зеркало"); + Исключение + Ожидаем.Что(ИнформацияОбОшибке().Описание, "сказано, что публиковать туда нечем") + .Содержит("публикацию не принимает"); + Возврат; + КонецПопытки; + + юТест.ПрерватьТест("Ожидали отказ: файловое зеркало публикацию не принимает"); + +КонецПроцедуры + +// Канал версии и заголовок OAUTH-TOKEN — свойства хаба oscript.io, а не пустого пула. +Процедура ТестДолжен_ОтличитьЛегасиХабОтОстальных() Экспорт + + Ожидаем.Что(Пуш.ЭтоЛегасиХаб(КонстантыOpm.СерверУдаленногоХранилища), "основной хаб").ЭтоИстина(); + Ожидаем.Что(Пуш.ЭтоЛегасиХаб(КонстантыOpm.СерверЗапасногоХранилища), "запасной хаб").ЭтоИстина(); + Ожидаем.Что(Пуш.ЭтоЛегасиХаб("http://localhost"), "свой хаб").ЭтоЛожь(); + Ожидаем.Что(Пуш.ЭтоЛегасиХаб(""), "пустой адрес").ЭтоЛожь(); + +КонецПроцедуры + +//////////////////////////////////////////////////////////////////////////////// +// Разбор командной строки + +Процедура ТестДолжен_ОтклонитьАдресВПозиционномАргументе() Экспорт + + Команда = Команда(); + Команда.ЗадатьАргумент("FILE", "localhost:3380/corp"); + + Попытка + Пуш.ВыполнитьКоманду(Команда); + Исключение + Ожидаем.Что(ИнформацияОбОшибке().Описание, "названа опция").Содержит("--hub"); + Возврат; + КонецПопытки; + + юТест.ПрерватьТест("Ожидали отказ: адрес хаба в позиционном аргументе больше не разбирается"); + +КонецПроцедуры + +// Устаревшая опция принимает только имя канала: адрес пула указывается опцией <--hub>. +Процедура ТестДолжен_НеПринятьАдресПулаВКанале() Экспорт + + Команда = Команда(); + Команда.ЗадатьОпцию("channel", "localhost:3380/other"); + + Попытка + Пуш.ВыполнитьКоманду(Команда); + Исключение + Ожидаем.Что(ИнформацияОбОшибке().Описание, "адрес не признан именем канала") + .Содержит("недопустимое имя канала"); + Возврат; + КонецПопытки; + + юТест.ПрерватьТест("Ожидали отказ: адрес пула — не имя канала"); + +КонецПроцедуры + +//////////////////////////////////////////////////////////////////////////////// +// Чем авторизуемся + +Процедура ТестДолжен_ПубликоватьТокеномКонвейераПоверхТокенаКоманды() Экспорт + + Команда = Команда(); + Команда.ЗадатьОпцию("token", "личный-pat"); + + Авторизация = Пуш.ВыбратьАвторизацию(Команда, Пуш.ЦельПубликации("localhost:3380/corp"), + ЗаглушкаКонвейера(Истина, Истина, ТокенПробы())); + + Ожидаем.Что(Авторизация.Значение, "публикуем id-token'ом").Равно("Bearer " + ТокенПробы()); + Ожидаем.Что(Авторизация.Доверенная, "публикация доверенная").ЭтоИстина(); + +КонецПроцедуры + +Процедура ТестДолжен_ВзятьТокенКомандыБезДоверия() Экспорт + + Команда = Команда(); + Команда.ЗадатьОпцию("token", "личный-pat"); + + Авторизация = Пуш.ВыбратьАвторизацию(Команда, Пуш.ЦельПубликации("localhost:3380/corp"), + ЗаглушкаКонвейера(Ложь, Ложь, "")); + + Ожидаем.Что(Авторизация.Заголовок, "заголовок OpenHub").Равно("Authorization"); + Ожидаем.Что(Авторизация.Значение, "токен из команды").Равно("Bearer личный-pat"); + Ожидаем.Что(Авторизация.Доверенная, "публикация обычная").ЭтоЛожь(); + +КонецПроцедуры + +Процедура ТестДолжен_ВзятьАвторизациюИзОписанияСервера() Экспорт + + НастройкиOpm.ДобавитьСерверПакетов(СерверИзНастроек("свой", "http://localhost", 3391, + "/download/", "/push", "Bearer сохранённый")); + + Авторизация = Пуш.ВыбратьАвторизацию(Команда(), Пуш.ЦельПубликации(""), + ЗаглушкаКонвейера(Ложь, Ложь, "")); + + Ожидаем.Что(Авторизация.Значение, "вход из opm.cfg").Равно("Bearer сохранённый"); + +КонецПроцедуры + +// Легаси-хаб читает токен из OAUTH-TOKEN и понимает под ним токен GitHub. +Процедура ТестДолжен_ОтдатьТокенЛегасиХабуЗаголовкомOauthToken() Экспорт + + Команда = Команда(); + Команда.ЗадатьОпцию("token", "токен-github"); + + Авторизация = Пуш.ВыбратьАвторизацию(Команда, Пуш.ЦельПубликации(""), + ЗаглушкаКонвейера(Ложь, Ложь, "")); + + Ожидаем.Что(Авторизация.Заголовок, "заголовок легаси-хаба").Равно("OAUTH-TOKEN"); + Ожидаем.Что(Авторизация.Значение, "токен как есть").Равно("токен-github"); + +КонецПроцедуры + +// Про недостающее permissions: id-token: write человек должен узнать из отказа, +// а не только из отладочного журнала. +Процедура ТестДолжен_ОбъяснитьОтсутствиеТокенаПричинойБезДоверия() Экспорт + + Заглушка = ЗаглушкаКонвейера(Ложь, Ложь, ""); + Заглушка.Обнаруживать(Ложь, "вокруг GitHub Actions, но id-token конвейеру не выдан"); + + Попытка + Пуш.ВыбратьАвторизацию(Команда(), Пуш.ЦельПубликации("localhost:3380/corp"), Заглушка); + Исключение + Описание = ИнформацияОбОшибке().Описание; + Ожидаем.Что(Описание, "сказано, что нужен токен").Содержит("нужен токен"); + Ожидаем.Что(Описание, "названа причина отсутствия доверия").Содержит("id-token конвейеру не выдан"); + Возврат; + КонецПопытки; + + юТест.ПрерватьТест("Ожидали отказ: ни доверия, ни токена"); + +КонецПроцедуры + +//////////////////////////////////////////////////////////////////////////////// +// Повторная публикация того же номера версии + +// Поднятый флаг cli отдаёт строкой "True" — ровно так его видит настоящая команда, +// и ровно на этом ломается сравнение значения опции с Истиной. Единицу человек пишет +// вслед за параметром запроса force=1, а платформа её к булеву не приводит. +Процедура ТестДолжен_УвидетьПринудительнуюПубликациюВОпцииForce() Экспорт + + Ожидаем.Что(Пуш.ЭтоПринудительнаяПубликация(КомандаСОпциейForce("True")), + "флаг поднят: cli отдал строку").ЭтоИстина(); + Ожидаем.Что(Пуш.ЭтоПринудительнаяПубликация(КомандаСОпциейForce(Истина)), + "флаг поднят: значение уже булево").ЭтоИстина(); + Ожидаем.Что(Пуш.ЭтоПринудительнаяПубликация(КомандаСОпциейForce("1")), + "написано --force=1").ЭтоИстина(); + +КонецПроцедуры + +// Неподнятый флаг приходит от cli булевой Ложью, а от команды, которая об опции не знает +// вовсе, — пустым значением. Ни то ни другое принудительной публикацией не является. +Процедура ТестДолжен_СчитатьПубликациюОбычнойБезОпцииForce() Экспорт + + Ожидаем.Что(Пуш.ЭтоПринудительнаяПубликация(КомандаСОпциейForce(Ложь)), + "флаг не поднят").ЭтоЛожь(); + Ожидаем.Что(Пуш.ЭтоПринудительнаяПубликация(Команда()), + "опции нет вовсе").ЭтоЛожь(); + Ожидаем.Что(Пуш.ЭтоПринудительнаяПубликация(КомандаСОпциейForce("0")), + "написано --force=0").ЭтоЛожь(); + +КонецПроцедуры + +// Непонятое значение не должно тихо становиться обычной публикацией: человек написал его +// не зря, а узнать о том, что опция не сработала, ему больше неоткуда. +Процедура ТестДолжен_ОтказатьНаНепонятномЗначенииОпцииForce() Экспорт + + Попытка + Пуш.ЭтоПринудительнаяПубликация(КомандаСОпциейForce("ага")); + Исключение + Описание = ИнформацияОбОшибке().Описание; + Ожидаем.Что(Описание, "названа сама опция").Содержит("force"); + Ожидаем.Что(Описание, "сказано, что она флаг").Содержит("флаг"); + Возврат; + КонецПопытки; + + юТест.ПрерватьТест("Ожидали отказ на непонятном значении опции <--force>"); + +КонецПроцедуры + +Процедура ТестДолжен_ДописатьПараметрForceКМаршрутуПубликации() Экспорт + + Ожидаем.Что(Отправитель().АдресПубликации("/api/v1/pools/corp/push", Истина), "адрес с force") + .Равно("/api/v1/pools/corp/push?force=1"); + +КонецПроцедуры + +Процедура ТестДолжен_ОставитьМаршрутПубликацииБезПараметровБезОпцииForce() Экспорт + + Ожидаем.Что(Отправитель().АдресПубликации("/api/v1/pools/corp/push", Ложь), "адрес как прежде") + .Равно("/api/v1/pools/corp/push"); + +КонецПроцедуры + +// Маршрут публикации приходит из настроек сервера, а туда его пишет человек — со строкой +// запроса в том числе. +Процедура ТестДолжен_ДописатьForceКМаршрутуСоСтрокойЗапроса() Экспорт + + Ожидаем.Что(Отправитель().АдресПубликации("/push?owner=corp", Истина), "force вторым параметром") + .Равно("/push?owner=corp&force=1"); + +КонецПроцедуры + +// Единственное место, где видно ровно то, что уходит хабу: маршрут публикации пула +// и параметр force ровно тогда, когда публикацию потребовали принудительной. +Процедура ТестДолжен_ОтправитьХабуПушСПараметромForce() Экспорт + + Стенд = ЖурналирующийХаб(); + Стенд.Поднять(юТест.ИмяВременногоФайла()); + + Попытка + Цель = Пуш.ЦельПубликации(СтрШаблон("127.0.0.1:%1/corp", Стенд.Порт)); + Авторизация = Пуш.ВыбратьАвторизацию(КомандаСТокеном(), Цель, ЗаглушкаКонвейера(Ложь, Ложь, "")); + + Пуш.Опубликовать(Цель, Авторизация, ФайлПакетаПробы(), "stable", Истина); + Пуш.Опубликовать(Цель, Авторизация, ФайлПакетаПробы(), "stable", Ложь); + Исключение + Стенд.Остановить(); + ВызватьИсключение; + КонецПопытки; + + Стенд.Остановить(); + + Запросы = Стенд.Запросы(); + + Ожидаем.Что(Запросы.Количество(), "хаб принял две публикации").Равно(2); + Ожидаем.Что(Запросы[0], "принудительная публикация несёт force") + .Равно("POST /api/v1/pools/corp/push?force=1"); + Ожидаем.Что(Запросы[1], "обычная публикация уходит как раньше") + .Равно("POST /api/v1/pools/corp/push"); + +КонецПроцедуры + +// Шов от командной строки до провода целиком. Половины этого шва зелены и порознь, и при +// разорванной середине: незарегистрированную опцию заглушка команды всё равно вернёт, +// а флаг, потерянный по дороге к отправителю, не изменит ни одного значения — обе поломки +// видны только здесь, где строку команды разбирает настоящий cli. +Процедура ТестДолжен_ДонестиОпциюForceОтКоманднойСтрокиДоЗапроса() Экспорт + + Стенд = ЖурналирующийХаб(); + Стенд.Поднять(юТест.ИмяВременногоФайла()); + + Попытка + ОпубликоватьКоманднойСтрокой(Стенд.Порт, "--force"); + ОпубликоватьКоманднойСтрокой(Стенд.Порт, ""); + Исключение + Стенд.Остановить(); + ВызватьИсключение; + КонецПопытки; + + Стенд.Остановить(); + + Запросы = Стенд.Запросы(); + + Ожидаем.Что(Запросы.Количество(), "хаб принял две публикации").Равно(2); + Ожидаем.Что(Запросы[0], "opm push --force") + .Равно("POST /api/v1/pools/corp/push?force=1"); + Ожидаем.Что(Запросы[1], "opm push без опции") + .Равно("POST /api/v1/pools/corp/push"); + +КонецПроцедуры + +//////////////////////////////////////////////////////////////////////////////// + +Функция Команда() + + Команда = ЗагрузитьЗаглушку("ЗаглушкаКомандыПриложения.os"); + Команда.ЗадатьОпцию("channel", "auto"); + Команда.ЗадатьОпцию("token-stdin", Ложь); + + Возврат Команда; + +КонецФункции + +Функция КомандаСОпциейForce(Знач Значение) + + Команда = Команда(); + Команда.ЗадатьОпцию("force", Значение); + + Возврат Команда; + +КонецФункции + +// Токен уезжает заголовком, а заголовки платформа отдаёт только в ASCII: кириллица в токене +// уронила бы публикацию на стенд ещё до того, как адрес дойдёт до хаба. +Функция КомандаСТокеном() + + Команда = Команда(); + Команда.ЗадатьОпцию("token", ТокенПробы()); + + Возврат Команда; + +КонецФункции + +Функция Отправитель() + + Возврат Новый ОтправительПакета("http://localhost", 3380); + +КонецФункции + +// Публикация тем же путём, каким её запускает пользователь: строку команды разбирает +// настоящий cli, поэтому опция, не объявленная в описании команды, здесь не «значение +// по умолчанию», а отказ разбора. +Процедура ОпубликоватьКоманднойСтрокой(Знач ПортСтенда, Знач Опция) + + Аргументы = Новый Массив(); + Аргументы.Добавить("push"); + Аргументы.Добавить("--hub"); + Аргументы.Добавить(СтрШаблон("127.0.0.1:%1/corp", ПортСтенда)); + Аргументы.Добавить("--token"); + Аргументы.Добавить(ТокенПробы()); + + Если НЕ ПустаяСтрока(Опция) Тогда + Аргументы.Добавить(Опция); + КонецЕсли; + + Аргументы.Добавить(ФайлПакетаПробы().ПолноеИмя); + + Приложение = Новый КонсольноеПриложение("opm", "публикация в журналирующий хаб"); + Приложение.ДобавитьКоманду("p push", "публикация", Новый КомандаOpm_Push); + Приложение.Запустить(Аргументы); + +КонецПроцедуры + +// Стенд с живым процессом заглушкой не является, но лежит там же, где фикстуры набора. +Функция ЖурналирующийХаб() + + Возврат ЗагрузитьСценарий(ОбъединитьПути(ТекущийСценарий().Каталог, "fixtures", + "ЖурналирующийХаб.os")); + +КонецФункции + +Функция ФайлПакетаПробы() + + ПутьКФайлу = ВременныеФайлы.НовоеИмяФайла("ospx"); + + Запись = Новый ЗаписьТекста(ПутьКФайлу, КодировкаТекста.UTF8); + Запись.Записать("содержимое пакета"); + Запись.Закрыть(); + + Возврат Новый Файл(ПутьКФайлу); + +КонецФункции + +Функция СерверИзНастроек(Знач Имя, Знач Адрес, Знач Порт, Знач ПутьВыдачи, Знач ПутьПубликации, + Знач Авторизация) + + Возврат Новый СерверПакетов(Имя, Адрес, ПутьВыдачи, ПутьПубликации, Порт, 0, + Авторизация, Новый Соответствие(), 600); + +КонецФункции + +Функция ЗаглушкаКонвейера(Знач Обнаружено, Знач Успех, Знач Токен) + + Заглушка = ЗагрузитьЗаглушку("ЗаглушкаКонвейера.os"); + Заглушка.Обнаруживать(Обнаружено, "заглушка видит конвейер"); + Заглушка.Отдавать(Успех, Токен); + + Возврат Заглушка; + +КонецФункции + +Функция ЗагрузитьЗаглушку(Знач ИмяФайла) + Возврат ЗагрузитьСценарий(ОбъединитьПути(ТекущийСценарий().Каталог, "fixtures", ИмяФайла)); +КонецФункции + +Функция ТокенПробы() + Возврат "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJnaCJ9.c2ln"; +КонецФункции diff --git a/tests/trusted-publish-test.os b/tests/trusted-publish-test.os new file mode 100644 index 0000000..c430ce9 --- /dev/null +++ b/tests/trusted-publish-test.os @@ -0,0 +1,410 @@ +#Использовать asserts +#Использовать tempfiles +#Использовать "../src/core" +#Использовать "../src/cmd" + +Перем юТест; +Перем Запросчик; +Перем Пуш; + +Функция ПолучитьСписокТестов(Знач Тестирование) Экспорт + + юТест = Тестирование; + + СписокТестов = Новый Массив; + + СписокТестов.Добавить("ТестДолжен_ВзятьТокенУСервисаТокеновGitHub"); + СписокТестов.Добавить("ТестДолжен_ДописатьАудиториюКАдресуСПараметрами"); + СписокТестов.Добавить("ТестДолжен_ОбъяснитьОтсутствиеРазрешенияIdToken"); + СписокТестов.Добавить("ТестДолжен_ОбъяснитьОтказСервисаТокенов"); + СписокТестов.Добавить("ТестДолжен_СообщитьОНедоступностиСервисаТокенов"); + СписокТестов.Добавить("ТестДолжен_ОтказатьБезПоляValueВОтвете"); + СписокТестов.Добавить("ТестДолжен_ВзятьТокенИзНазваннойПеременнойGitLab"); + СписокТестов.Добавить("ТестДолжен_ВзятьТокенИзПеременнойПоУмолчанию"); + СписокТестов.Добавить("ТестДолжен_ПодсказатьПроIdTokensВGitLabБезПеременной"); + СписокТестов.Добавить("ТестДолжен_ОтказатьПриПустойПеременнойКонвейера"); + СписокТестов.Добавить("ТестДолжен_ОтвергнутьЗначениеНеПохожееНаIdToken"); + СписокТестов.Добавить("ТестДолжен_ОтвергнутьТокенСПустымСегментом"); + СписокТестов.Добавить("ТестДолжен_ПотребоватьАудиторию"); + СписокТестов.Добавить("ТестДолжен_ОбнаружитьДовериеПоПеременнойПоУмолчанию"); + СписокТестов.Добавить("ТестДолжен_ОбнаружитьДовериеПоСервисуGitHub"); + СписокТестов.Добавить("ТестДолжен_УвидетьСервисТокеновТолькоСОбеимиПеременными"); + СписокТестов.Добавить("ТестДолжен_НеОбнаружитьДовериеВнеКонвейера"); + СписокТестов.Добавить("ТестДолжен_СчитатьНазваннуюПеременнуюНенайденнойБезЗначения"); + СписокТестов.Добавить("ТестДолжен_ВзятьТокенКонвейераКогдаОнОбнаружен"); + СписокТестов.Добавить("ТестДолжен_ОтступитьНаТокенКогдаДовериеНеОбнаружено"); + СписокТестов.Добавить("ТестДолжен_ОтказатьКогдаНазваннаяПеременнаяПуста"); + СписокТестов.Добавить("ТестДолжен_ОтказатьКогдаТокенКонвейераНеВыдан"); + СписокТестов.Добавить("ТестДолжен_ПредпочестьЗаданнуюАудиторию"); + СписокТестов.Добавить("ТестДолжен_ПредупредитьОЧужойАудитории"); + СписокТестов.Добавить("ТестДолжен_ОбъяснитьНеудачуДоверенннойПубликации"); + СписокТестов.Добавить("ТестДолжен_ОставитьСообщениеХабаНаОбычнойПубликации"); + + Возврат СписокТестов; + +КонецФункции + +Процедура ПередЗапускомТеста() Экспорт + + Запросчик = ЗагрузитьЗаглушку("ЗаглушкаЗапросчика.os"); + Пуш = Новый КомандаOpm_Push(); + +КонецПроцедуры + +Процедура ПослеЗапускаТеста() Экспорт + + ВременныеФайлы.Удалить(); + +КонецПроцедуры + +//////////////////////////////////////////////////////////////////////////////// +// GitHub Actions: токен заказывается у сервиса токенов + +Процедура ТестДолжен_ВзятьТокенУСервисаТокеновGitHub() Экспорт + + Запросчик.Отвечать(200, "{""value"": """ + ТокенПробы() + """}"); + + Результат = Конвейер(ОкружениеGitHub()).Получить(АдресХаба()); + + Ожидаем.Что(Результат.Успех, "токен получен").ЭтоИстина(); + Ожидаем.Что(Результат.Токен, "значение токена").Равно(ТокенПробы()); + Ожидаем.Что(Результат.Источник, "источник токена").Равно("GitHub Actions"); + + Запрос = Запросчик.Запросы[0]; + Ожидаем.Что(Запрос.Адрес, "аудитория ушла параметром") + .Содержит("audience=" + КодироватьСтроку(АдресХаба(), СпособКодированияСтроки.КодировкаURL)); + Ожидаем.Что(Запрос.Заголовки["Authorization"], "запрос авторизован токеном запроса") + .Равно("Bearer req-token-abc"); + +КонецПроцедуры + +Процедура ТестДолжен_ДописатьАудиториюКАдресуСПараметрами() Экспорт + + Запросчик.Отвечать(200, "{""value"": ""a.b.c""}"); + + Конвейер(ОкружениеGitHub()).Получить(АдресХаба()); + + Ожидаем.Что(Запросчик.Запросы[0].Адрес, "параметр дописан через амперсанд") + .Содержит("api-version=2.0&audience="); + +КонецПроцедуры + +Процедура ТестДолжен_ОбъяснитьОтсутствиеРазрешенияIdToken() Экспорт + + Окружение = Новый Соответствие(); + Окружение.Вставить("GITHUB_ACTIONS", "true"); + + Результат = Конвейер(Окружение).Получить(АдресХаба()); + + Ожидаем.Что(Результат.Успех, "токена нет").ЭтоЛожь(); + Ожидаем.Что(Результат.Сообщение, "сказано, чего не хватает в workflow") + .Содержит("permissions: id-token: write"); + +КонецПроцедуры + +Процедура ТестДолжен_ОбъяснитьОтказСервисаТокенов() Экспорт + + Запросчик.Отвечать(403, ""); + + Результат = Конвейер(ОкружениеGitHub()).Получить(АдресХаба()); + + Ожидаем.Что(Результат.Успех, "токена нет").ЭтоЛожь(); + Ожидаем.Что(Результат.Сообщение, "отказ объяснён по-человечески") + .Содержит("permissions: id-token: write"); + +КонецПроцедуры + +Процедура ТестДолжен_СообщитьОНедоступностиСервисаТокенов() Экспорт + + Запросчик.Отвечать(0, "Не удалось подключиться к https://token.example: соединение отклонено"); + + Результат = Конвейер(ОкружениеGitHub()).Получить(АдресХаба()); + + Ожидаем.Что(Результат.Успех, "токена нет").ЭтоЛожь(); + Ожидаем.Что(Результат.Сообщение, "причина связи не потеряна").Содержит("соединение отклонено"); + +КонецПроцедуры + +Процедура ТестДолжен_ОтказатьБезПоляValueВОтвете() Экспорт + + Запросчик.Отвечать(200, "{""другое"": ""значение""}"); + + Результат = Конвейер(ОкружениеGitHub()).Получить(АдресХаба()); + + Ожидаем.Что(Результат.Успех, "токена нет").ЭтоЛожь(); + Ожидаем.Что(Результат.Сообщение, "названо недостающее поле").Содержит("value"); + +КонецПроцедуры + +//////////////////////////////////////////////////////////////////////////////// +// Переменные среды: имя называют опцией либо берут по соглашению + +Процедура ТестДолжен_ВзятьТокенИзНазваннойПеременнойGitLab() Экспорт + + Результат = Конвейер(ОкружениеGitLab("OPENHUB_TOKEN", ТокенПробы())).Получить(АдресХаба(), "OPENHUB_TOKEN"); + + Ожидаем.Что(Результат.Успех, "токен получен").ЭтоИстина(); + Ожидаем.Что(Результат.Токен, "значение токена").Равно(ТокенПробы()); + Ожидаем.Что(Запросчик.Запросы.Количество(), "сеть не понадобилась").Равно(0); + +КонецПроцедуры + +// Имя переменной GitLab назначает конвейер, поэтому без подсказки клиент знает +// только соглашение: переменную id_token. +Процедура ТестДолжен_ВзятьТокенИзПеременнойПоУмолчанию() Экспорт + + Результат = Конвейер(ОкружениеGitLab("id_token", ТокенПробы())).Получить(АдресХаба()); + + Ожидаем.Что(Результат.Успех, "токен взят без подсказки").ЭтоИстина(); + Ожидаем.Что(Результат.Токен, "значение токена").Равно(ТокенПробы()); + Ожидаем.Что(Запросчик.Запросы.Количество(), "сеть не понадобилась").Равно(0); + +КонецПроцедуры + +Процедура ТестДолжен_ПодсказатьПроIdTokensВGitLabБезПеременной() Экспорт + + Результат = Конвейер(ОкружениеGitLab("ЧУЖАЯ", ТокенПробы())).Получить(АдресХаба()); + + Ожидаем.Что(Результат.Успех, "переменную не нашли").ЭтоЛожь(); + Ожидаем.Что(Результат.Сообщение, "сказано про секцию id_tokens").Содержит("id_tokens"); + Ожидаем.Что(Результат.Сообщение, "названа опция").Содержит("--oidc-token-var"); + +КонецПроцедуры + +Процедура ТестДолжен_ОтказатьПриПустойПеременнойКонвейера() Экспорт + + Результат = Конвейер(ОкружениеGitLab("OPENHUB_TOKEN", "")).Получить(АдресХаба(), "OPENHUB_TOKEN"); + + Ожидаем.Что(Результат.Успех, "переменная пуста").ЭтоЛожь(); + Ожидаем.Что(Результат.Сообщение, "названа переменная").Содержит("OPENHUB_TOKEN"); + +КонецПроцедуры + +Процедура ТестДолжен_ОтвергнутьЗначениеНеПохожееНаIdToken() Экспорт + + Результат = Конвейер(ОкружениеGitLab("OPENHUB_TOKEN", "oshub_обычный_pat")) + .Получить(АдресХаба(), "OPENHUB_TOKEN"); + + Ожидаем.Что(Результат.Успех, "это не id-token").ЭтоЛожь(); + Ожидаем.Что(Результат.Сообщение, "сказано, чего ждали").Содержит("трёх сегментов"); + +КонецПроцедуры + +Процедура ТестДолжен_ОтвергнутьТокенСПустымСегментом() Экспорт + + Результат = Конвейер(ОкружениеGitLab("OPENHUB_TOKEN", "a.b.")).Получить(АдресХаба(), "OPENHUB_TOKEN"); + + Ожидаем.Что(Результат.Успех, "третий сегмент пуст").ЭтоЛожь(); + Ожидаем.Что(Результат.Сообщение, "сказано, чего ждали").Содержит("трёх сегментов"); + +КонецПроцедуры + +Процедура ТестДолжен_ПотребоватьАудиторию() Экспорт + + Результат = Конвейер(ОкружениеGitHub()).Получить(""); + + Ожидаем.Что(Результат.Успех, "без аудитории токен не заказывается").ЭтоЛожь(); + Ожидаем.Что(Результат.Сообщение, "сказано про клейм aud").Содержит("aud"); + Ожидаем.Что(Запросчик.Запросы.Количество(), "запроса не было").Равно(0); + +КонецПроцедуры + +//////////////////////////////////////////////////////////////////////////////// +// Обнаружение доверия: решает набор переменных среды, а не отдельная опция + +Процедура ТестДолжен_ОбнаружитьДовериеПоПеременнойПоУмолчанию() Экспорт + + Обнаруженное = Конвейер(ОкружениеGitLab("id_token", ТокенПробы())).Обнаружить(); + + Ожидаем.Что(Обнаруженное.Найдено, "переменная id_token заполнена").ЭтоИстина(); + Ожидаем.Что(Обнаруженное.ИмяПеременной, "имя переменной").Равно("id_token"); + Ожидаем.Что(Обнаруженное.Причина, "причина названа человеку").Содержит("id_token"); + +КонецПроцедуры + +Процедура ТестДолжен_ОбнаружитьДовериеПоСервисуGitHub() Экспорт + + Обнаруженное = Конвейер(ОкружениеGitHub()).Обнаружить(); + + Ожидаем.Что(Обнаруженное.Найдено, "конвейеру выдан доступ к сервису токенов").ЭтоИстина(); + Ожидаем.Что(Обнаруженное.Способ, "токен закажем у сервиса").Равно("сервис"); + Ожидаем.Что(Обнаруженное.Причина, "причина названа человеку").Содержит("GitHub Actions"); + +КонецПроцедуры + +Процедура ТестДолжен_УвидетьСервисТокеновТолькоСОбеимиПеременными() Экспорт + + Половина = ОкружениеGitHub(); + Половина.Удалить("ACTIONS_ID_TOKEN_REQUEST_TOKEN"); + + Обнаруженное = Конвейер(Половина).Обнаружить(); + + Ожидаем.Что(Обнаруженное.Найдено, "запрашивать токен нечем").ЭтоЛожь(); + Ожидаем.Что(Обнаруженное.Причина, "сказано, чего не хватает в workflow") + .Содержит("permissions: id-token: write"); + +КонецПроцедуры + +Процедура ТестДолжен_НеОбнаружитьДовериеВнеКонвейера() Экспорт + + Обнаруженное = Конвейер(Новый Соответствие()).Обнаружить(); + + Ожидаем.Что(Обнаруженное.Найдено, "конвейера вокруг нет").ЭтоЛожь(); + Ожидаем.Что(Обнаруженное.Причина, "в отладку уйдёт внятная причина").Содержит("id_token"); + +КонецПроцедуры + +// Названная опцией переменная — заявленное намерение: её отсутствие обязано быть видно, +// иначе вместо готового объяснения человек получит общее «нужен токен». +Процедура ТестДолжен_СчитатьНазваннуюПеременнуюНенайденнойБезЗначения() Экспорт + + Обнаруженное = Конвейер(ОкружениеGitLab("id_token", ТокенПробы())).Обнаружить("OPENHUB_TOKEN"); + + Ожидаем.Что(Обнаруженное.Найдено, "названной переменной нет").ЭтоЛожь(); + Ожидаем.Что(Обнаруженное.Причина, "названа именно она").Содержит("OPENHUB_TOKEN"); + +КонецПроцедуры + +//////////////////////////////////////////////////////////////////////////////// +// Решение opm push: обнаружили доверие — публикуем им, не обнаружили — токеном + +Процедура ТестДолжен_ВзятьТокенКонвейераКогдаОнОбнаружен() Экспорт + + Заглушка = ЗаглушкаКонвейера(Истина, Истина, ТокенПробы()); + + Токен = Пуш.ТокенДоверенногоКонвейера("", "", АдресХаба(), Заглушка); + + Ожидаем.Что(Токен, "публикуем токеном конвейера").Равно(ТокенПробы()); + Ожидаем.Что(Заглушка.ПоследняяАудитория, "аудитория — адрес хаба публикации").Равно(АдресХаба()); + +КонецПроцедуры + +Процедура ТестДолжен_ОтступитьНаТокенКогдаДовериеНеОбнаружено() Экспорт + + Заглушка = ЗаглушкаКонвейера(Ложь, Ложь, ""); + + Токен = Пуш.ТокенДоверенногоКонвейера("", "", АдресХаба(), Заглушка); + + Ожидаем.Что(Токен, "доверенной публикации нет").Равно(""); + Ожидаем.Что(Заглушка.ОбращенийБыло, "за токеном не ходили").Равно(0); + +КонецПроцедуры + +Процедура ТестДолжен_ОтказатьКогдаНазваннаяПеременнаяПуста() Экспорт + + Заглушка = ЗаглушкаКонвейера(Ложь, Ложь, ""); + Заглушка.Обнаруживать(Ложь, "переменная среды OPENHUB_TOKEN не задана либо пуста"); + + Попытка + Пуш.ТокенДоверенногоКонвейера("OPENHUB_TOKEN", "", АдресХаба(), Заглушка); + Исключение + Ожидаем.Что(ИнформацияОбОшибке().Описание, "заготовленное объяснение доходит до человека") + .Содержит("переменная среды OPENHUB_TOKEN не задана либо пуста"); + Возврат; + КонецПопытки; + + юТест.ПрерватьТест("Ожидали отказ: переменная названа, а токена в ней нет"); + +КонецПроцедуры + +Процедура ТестДолжен_ОтказатьКогдаТокенКонвейераНеВыдан() Экспорт + + Заглушка = ЗаглушкаКонвейера(Истина, Ложь, ""); + Заглушка.Отдавать(Ложь, "", "сервис токенов GitHub ответил кодом 500"); + + Попытка + Пуш.ТокенДоверенногоКонвейера("", "", АдресХаба(), Заглушка); + Исключение + Ожидаем.Что(ИнформацияОбОшибке().Описание, "причина названа") + .Содержит("сервис токенов GitHub ответил кодом 500"); + Возврат; + КонецПопытки; + + юТест.ПрерватьТест("Ожидали отказ команды: доверие обнаружено, а токен не выдан"); + +КонецПроцедуры + +Процедура ТестДолжен_ПредпочестьЗаданнуюАудиторию() Экспорт + + Заглушка = ЗаглушкаКонвейера(Истина, Истина, ТокенПробы()); + + Пуш.ТокенДоверенногоКонвейера("", "https://hub.example.com", АдресХаба(), Заглушка); + + Ожидаем.Что(Заглушка.ПоследняяАудитория, "аудитория взята из опции") + .Равно("https://hub.example.com"); + +КонецПроцедуры + +Процедура ТестДолжен_ПредупредитьОЧужойАудитории() Экспорт + + Ожидаем.Что(Пуш.ПредупреждениеОбАудитории(АдресХаба(), АдресХаба()), + "аудитория совпадает с хабом").Равно(""); + + Ожидаем.Что(Пуш.ПредупреждениеОбАудитории("https://hub.example.com", АдресХаба()), + "названы обе стороны расхождения").Содержит("https://hub.example.com"); + +КонецПроцедуры + +// Поддержку доверия и запись доверия ведёт хаб: клиенту виден только код ответа, +// поэтому отказ на доверенной публикации называется своим именем. +Процедура ТестДолжен_ОбъяснитьНеудачуДоверенннойПубликации() Экспорт + + Отказ = Новый Структура("Код, Сообщение, Успех", 403, "нет записи доверия", Ложь); + + Сообщение = Пуш.СообщениеОбОтказеПубликации(Отказ, Истина, "http://localhost:3380/corp"); + + Ожидаем.Что(Сообщение, "названа доверенная публикация").Содержит("Доверенная публикация"); + Ожидаем.Что(Сообщение, "код ответа хаба на месте").Содержит("403"); + Ожидаем.Что(Сообщение, "сообщение хаба не потеряно").Содержит("нет записи доверия"); + +КонецПроцедуры + +Процедура ТестДолжен_ОставитьСообщениеХабаНаОбычнойПубликации() Экспорт + + Отказ = Новый Структура("Код, Сообщение, Успех", 401, "нужен токен", Ложь); + + Ожидаем.Что(Пуш.СообщениеОбОтказеПубликации(Отказ, Ложь, "http://localhost:3380"), + "обычная публикация объясняется хабом").Равно("нужен токен"); + +КонецПроцедуры + +//////////////////////////////////////////////////////////////////////////////// + +Функция АдресХаба() + Возврат "http://localhost:3380"; +КонецФункции + +Функция ТокенПробы() + Возврат "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJnaCJ9.c2ln"; +КонецФункции + +Функция Конвейер(Знач Переменные) + Возврат Новый ТокенКонвейераCI(Новый ОкружениеПроцесса(Переменные), Запросчик); +КонецФункции + +Функция ОкружениеGitHub() + Переменные = Новый Соответствие(); + Переменные.Вставить("GITHUB_ACTIONS", "true"); + Переменные.Вставить("ACTIONS_ID_TOKEN_REQUEST_URL", + "https://token.example/idtoken?api-version=2.0"); + Переменные.Вставить("ACTIONS_ID_TOKEN_REQUEST_TOKEN", "req-token-abc"); + Возврат Переменные; +КонецФункции + +Функция ОкружениеGitLab(Знач ИмяПеременной, Знач ЗначениеТокена) + Переменные = Новый Соответствие(); + Переменные.Вставить("GITLAB_CI", "true"); + Переменные.Вставить(ИмяПеременной, ЗначениеТокена); + Возврат Переменные; +КонецФункции + +Функция ЗаглушкаКонвейера(Знач Обнаружено, Знач Успех, Знач Токен) + Заглушка = ЗагрузитьЗаглушку("ЗаглушкаКонвейера.os"); + Заглушка.Обнаруживать(Обнаружено, "заглушка видит конвейер"); + Заглушка.Отдавать(Успех, Токен); + Возврат Заглушка; +КонецФункции + +Функция ЗагрузитьЗаглушку(Знач ИмяФайла) + Возврат ЗагрузитьСценарий(ОбъединитьПути(ТекущийСценарий().Каталог, "fixtures", ИмяФайла)); +КонецФункции diff --git a/tests/versions-test.os b/tests/versions-test.os index 785b11f..a24b2fd 100644 --- a/tests/versions-test.os +++ b/tests/versions-test.os @@ -17,6 +17,7 @@ СписокТестов.Добавить("ТестДолжен_ПолучитьИмяИВерсиюПоИмениПакетаБезВерсии"); СписокТестов.Добавить("ТестДолжен_ПолучитьИмяИВерсиюПоИмениПакетаСВерсией"); СписокТестов.Добавить("ТестДолжен_СравнитьЧетырехзначныеВерсии"); + СписокТестов.Добавить("ТестДолжен_ОтличитьНомерВерсииОтВыраженияИТега"); Возврат СписокТестов; @@ -110,3 +111,23 @@ Ожидаем.Что(РезультатСравнения).Меньше(0); КонецПроцедуры + +// Номер версии — то, чем может называться артефакт на хабе, который ничего не разрешает. +Процедура ТестДолжен_ОтличитьНомерВерсииОтВыраженияИТега() Экспорт + + Ожидаем.Что(РаботаСВерсиями.ЭтоТочныйНомерВерсии("1.2.3"), "релиз").Равно(Истина); + Ожидаем.Что(РаботаСВерсиями.ЭтоТочныйНомерВерсии("1.4"), "легаси-двойка").Равно(Истина); + Ожидаем.Что(РаботаСВерсиями.ЭтоТочныйНомерВерсии("1.2.3.4"), "легаси-четвёрка").Равно(Истина); + Ожидаем.Что(РаботаСВерсиями.ЭтоТочныйНомерВерсии("1.3.0-rc.1"), "пререлиз").Равно(Истина); + Ожидаем.Что(РаботаСВерсиями.ЭтоТочныйНомерВерсии("1.0.0+build"), "сборка").Равно(Истина); + Ожидаем.Что(РаботаСВерсиями.ЭтоТочныйНомерВерсии("SNAPSHOT"), "снапшот").Равно(Истина); + Ожидаем.Что(РаботаСВерсиями.ЭтоТочныйНомерВерсии("snapshot"), "снапшот строчными").Равно(Истина); + Ожидаем.Что(РаботаСВерсиями.ЭтоТочныйНомерВерсии("1.x"), "маска версии").Равно(Ложь); + Ожидаем.Что(РаботаСВерсиями.ЭтоТочныйНомерВерсии("^1.4"), "каретный диапазон").Равно(Ложь); + Ожидаем.Что(РаботаСВерсиями.ЭтоТочныйНомерВерсии("~1.4"), "тильдовый диапазон").Равно(Ложь); + Ожидаем.Что(РаботаСВерсиями.ЭтоТочныйНомерВерсии(">=1.0"), "диапазон со сравнением").Равно(Ложь); + Ожидаем.Что(РаботаСВерсиями.ЭтоТочныйНомерВерсии("stable"), "тег").Равно(Ложь); + Ожидаем.Что(РаботаСВерсиями.ЭтоТочныйНомерВерсии(""), "пустая спецификация").Равно(Ложь); + +КонецПроцедуры +