From 9737c787f9ec33bb3e4ba371f353acb4a0d0f73a Mon Sep 17 00:00:00 2001 From: zuevav Date: Fri, 19 Jun 2026 00:03:21 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=B6=D0=B8=D0=B2=D0=BE=D0=B9=20=D1=86?= =?UTF-8?q?=D0=B8=D0=BA=D0=BB=20M2M=20=D1=81=20=D0=9D=D0=A0=D0=94=20+=20?= =?UTF-8?q?=D0=BC=D0=B0=D1=81=D1=82=D0=B5=D1=80=20=D1=83=D1=81=D1=82=D0=B0?= =?UTF-8?q?=D0=BD=D0=BE=D0=B2=D0=BA=D0=B8=20=D0=BA=D0=BB=D1=8E=D1=87=D0=B0?= =?UTF-8?q?=20=D0=BD=D0=B0=20=D1=84=D0=BB=D0=B5=D1=88=D0=BA=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Инфраструктура M2M (живой обмен с НРД через ИШ): - обработка M2MTransferResponse: ERROR(M2Mxx) → заявка Отклонена, сохранение ответа; INFO → ждём Decision; идемпотентность поллера - fallback-корреляция ответов с нулевым GUID (M2M14/M2M17) по FIFO - сырой XML ответа НРД в карточке заявки (для пересылки в ТП) - тестовый пакет роботу приведён к эталону m2m_robot_samples (CostInfo=Yes, 4 бумаги, IsolationStatus, DocumentSeries=сценарий); override паспорта - редирект из теста сразу в карточку заявки Мастер установки ключа Валидаты на флешку (admin/setup/keywizard): - пошаговый: загрузка .7z+пароль → выбор флешки → запись → справочник сертификатов (CRL) → перезапуск+проверка ИШ → готово - привилегированный воркер (bj-keymedia) в host-namespace через файл-обмен, bj-server остаётся в песочнице - сохранение структуры профиля архива (spr), перечисление съёмных USB Прочее: - пакет-доказательство для ТП НРД + форма регистрации участника M2M - эталонные образцы робота (DOC/m2m_robot_samples) Co-Authored-By: Claude Opus 4.8 --- DOC/m2m_robot_samples/1111/README.txt | 8 + DOC/m2m_robot_samples/1111/error01/config.xml | 4 + .../1111/error01/request.xml | 119 +++ DOC/m2m_robot_samples/1111/error05/config.xml | 4 + .../1111/error05/request.xml | 119 +++ DOC/m2m_robot_samples/2001/README.txt | 8 + .../2001/all_to_1st_depo/config.xml | 4 + .../2001/all_to_1st_depo/request.xml | 119 +++ .../2001/mixed_1st_and_2nd_depos/config.xml | 4 + .../2001/mixed_1st_and_2nd_depos/request.xml | 119 +++ .../2002/accept1st/config.xml | 0 assets/moex-most-logo/README.md | 34 + assets/moex-most-logo/main/moex-most.jpg | Bin 0 -> 82857 bytes assets/moex-most-logo/main/moex-most.pdf | 348 +++++++ assets/moex-most-logo/main/moex-most.png | Bin 0 -> 13976 bytes assets/moex-most-logo/main/moex-most.svg | 0 cmd/bj-artifactory/main.go | 114 +++ cmd/bj-installer/main.go | 111 +++ cmd/bj-installer/precheck.go | 132 +++ cmd/bj-installer/server.go | 129 +++ cmd/bj-installer/sse.go | 86 ++ cmd/bj-installer/state.go | 140 +++ cmd/bj-installer/steps.go | 84 ++ cmd/bj-installer/steps_impl.go | 448 +++++++++ cmd/bj-installer/web/app.js | 168 ++++ cmd/bj-installer/web/index.html | 110 +++ cmd/bj-installer/web/style.css | 179 ++++ cmd/bj-license-server/main.go | 96 ++ cmd/bj-license/main.go | 192 ++++ cmd/bj-release/main.go | 169 ++++ cmd/bj-server/main.go | 15 +- deploy/artifactory/README.md | 79 ++ deploy/artifactory/artifactory.service | 21 + deploy/artifactory/nginx.conf | 44 + deploy/ish/channel-reference.txt | 1 + deploy/ish/configure-ish.sql | 132 +++ deploy/ish/params-reference.txt | 83 ++ deploy/license/README.md | 71 ++ deploy/linux/install-validata.sh | 388 ++++++++ .../M2MTransferParticipantForm.example.xml | 59 ++ deploy/nsd-registration/README.md | 58 ++ deploy/nsd-registration/email-draft.txt | 29 + .../01_outgoing_M2MTransferRequest.xml | 119 +++ .../02_incoming_M2MTransferResponse.xml | 9 + .../support-bundle/README.txt | 55 ++ .../nsd-registration/support-bundle/email.txt | 44 + deploy/systemd/bj-crypto.service | 35 + go.mod | 14 +- go.sum | 50 +- internal/cryptocli/client.go | 492 +++++----- internal/cryptocli/client_test.go | 46 +- internal/cryptocli/cryptopb/crypto.pb.go | 694 ++++++++++++++ internal/cryptocli/cryptopb/crypto_grpc.pb.go | 305 ++++++ internal/license/license.go | 187 ++++ internal/license/license_test.go | 65 ++ internal/lkgateway/admin.go | 57 +- internal/lkgateway/cacerts.go | 67 +- internal/lkgateway/checks.go | 49 +- internal/lkgateway/flashcontainers.go | 190 ---- internal/lkgateway/keywizard.go | 525 +++++++++++ internal/lkgateway/licensecheck.go | 79 ++ internal/lkgateway/media.go | 745 +++++++++++++++ internal/lkgateway/nsdpoller.go | 119 +++ internal/lkgateway/runtimeconfig.go | 68 +- internal/lkgateway/server.go | 82 +- internal/lkgateway/server_test.go | 17 +- internal/lkgateway/service.go | 93 +- internal/lkgateway/setup.go | 889 ++++++++++++------ internal/lkgateway/types.go | 10 + internal/lkgateway/updater.go | 204 ++++ .../lkgateway/web/templates/admin_claim.html | 43 +- .../lkgateway/web/templates/admin_help.html | 8 +- .../templates/admin_help_architecture.html | 21 +- .../web/templates/admin_help_crypto.html | 136 +++ .../web/templates/admin_help_cryptopro.html | 130 --- .../web/templates/admin_help_lk.html | 2 +- .../web/templates/admin_help_systems.html | 17 +- .../lkgateway/web/templates/admin_home.html | 104 +- .../web/templates/admin_keywizard.html | 106 +++ .../lkgateway/web/templates/admin_news.html | 2 +- .../lkgateway/web/templates/admin_setup.html | 656 +++++++------ .../lkgateway/web/templates/admin_status.html | 2 +- .../lkgateway/web/templates/admin_wizard.html | 175 +--- internal/lkgateway/web/templates/layout.html | 319 ++++++- internal/m2m/messages.go | 6 +- internal/m2m/messages_test.go | 28 + internal/m2mcore/deal.go | 66 ++ internal/m2mcore/enrich.go | 10 + internal/m2mcore/enrich_test.go | 51 + internal/m2mcore/pgrepo.go | 18 +- internal/m2mcore/service_response_test.go | 85 ++ internal/nsdadapter/igw/client.go | 25 +- internal/nsdadapter/igw/client_test.go | 24 +- internal/nsdadapter/sender.go | 25 +- internal/nsdadapter/sender_test.go | 7 +- internal/release/client.go | 110 +++ internal/release/manifest.go | 198 ++++ internal/release/manifest_test.go | 61 ++ internal/release/version.go | 36 + services/crypto-service/build.gradle.kts | 46 +- services/crypto-service/proto/crypto.proto | 35 + .../crypto/ActivateHandler.java | 42 + .../bridgeandjoins/crypto/CryptoServer.java | 34 +- .../crypto/CryptoServiceImpl.java | 32 +- .../bridgeandjoins/crypto/HealthHandler.java | 24 +- .../crypto/KeystoreProvider.java | 59 +- .../crypto/ShutdownHandler.java | 33 + .../bridgeandjoins/crypto/SignHandler.java | 89 +- .../crypto/ValidataProvider.java | 124 +++ .../bridgeandjoins/crypto/VerifyHandler.java | 135 ++- 110 files changed, 10771 insertions(+), 1690 deletions(-) create mode 100644 DOC/m2m_robot_samples/1111/README.txt create mode 100644 DOC/m2m_robot_samples/1111/error01/config.xml create mode 100644 DOC/m2m_robot_samples/1111/error01/request.xml create mode 100644 DOC/m2m_robot_samples/1111/error05/config.xml create mode 100644 DOC/m2m_robot_samples/1111/error05/request.xml create mode 100644 DOC/m2m_robot_samples/2001/README.txt create mode 100644 DOC/m2m_robot_samples/2001/all_to_1st_depo/config.xml create mode 100644 DOC/m2m_robot_samples/2001/all_to_1st_depo/request.xml create mode 100644 DOC/m2m_robot_samples/2001/mixed_1st_and_2nd_depos/config.xml create mode 100644 DOC/m2m_robot_samples/2001/mixed_1st_and_2nd_depos/request.xml create mode 100644 DOC/m2m_robot_samples/2002/accept1st/config.xml create mode 100644 assets/moex-most-logo/README.md create mode 100755 assets/moex-most-logo/main/moex-most.jpg create mode 100755 assets/moex-most-logo/main/moex-most.pdf create mode 100755 assets/moex-most-logo/main/moex-most.png create mode 100644 assets/moex-most-logo/main/moex-most.svg create mode 100644 cmd/bj-artifactory/main.go create mode 100644 cmd/bj-installer/main.go create mode 100644 cmd/bj-installer/precheck.go create mode 100644 cmd/bj-installer/server.go create mode 100644 cmd/bj-installer/sse.go create mode 100644 cmd/bj-installer/state.go create mode 100644 cmd/bj-installer/steps.go create mode 100644 cmd/bj-installer/steps_impl.go create mode 100644 cmd/bj-installer/web/app.js create mode 100644 cmd/bj-installer/web/index.html create mode 100644 cmd/bj-installer/web/style.css create mode 100644 cmd/bj-license-server/main.go create mode 100644 cmd/bj-license/main.go create mode 100644 cmd/bj-release/main.go create mode 100644 deploy/artifactory/README.md create mode 100644 deploy/artifactory/artifactory.service create mode 100644 deploy/artifactory/nginx.conf create mode 100644 deploy/ish/channel-reference.txt create mode 100644 deploy/ish/configure-ish.sql create mode 100644 deploy/ish/params-reference.txt create mode 100644 deploy/license/README.md create mode 100755 deploy/linux/install-validata.sh create mode 100644 deploy/nsd-registration/M2MTransferParticipantForm.example.xml create mode 100644 deploy/nsd-registration/README.md create mode 100644 deploy/nsd-registration/email-draft.txt create mode 100644 deploy/nsd-registration/support-bundle/01_outgoing_M2MTransferRequest.xml create mode 100644 deploy/nsd-registration/support-bundle/02_incoming_M2MTransferResponse.xml create mode 100644 deploy/nsd-registration/support-bundle/README.txt create mode 100644 deploy/nsd-registration/support-bundle/email.txt create mode 100644 deploy/systemd/bj-crypto.service create mode 100644 internal/cryptocli/cryptopb/crypto.pb.go create mode 100644 internal/cryptocli/cryptopb/crypto_grpc.pb.go create mode 100644 internal/license/license.go create mode 100644 internal/license/license_test.go delete mode 100644 internal/lkgateway/flashcontainers.go create mode 100644 internal/lkgateway/keywizard.go create mode 100644 internal/lkgateway/licensecheck.go create mode 100644 internal/lkgateway/media.go create mode 100644 internal/lkgateway/nsdpoller.go create mode 100644 internal/lkgateway/updater.go create mode 100644 internal/lkgateway/web/templates/admin_help_crypto.html delete mode 100644 internal/lkgateway/web/templates/admin_help_cryptopro.html create mode 100644 internal/lkgateway/web/templates/admin_keywizard.html create mode 100644 internal/m2mcore/service_response_test.go create mode 100644 internal/release/client.go create mode 100644 internal/release/manifest.go create mode 100644 internal/release/manifest_test.go create mode 100644 internal/release/version.go create mode 100644 services/crypto-service/src/main/java/ru/zetit/bridgeandjoins/crypto/ActivateHandler.java create mode 100644 services/crypto-service/src/main/java/ru/zetit/bridgeandjoins/crypto/ShutdownHandler.java create mode 100644 services/crypto-service/src/main/java/ru/zetit/bridgeandjoins/crypto/ValidataProvider.java diff --git a/DOC/m2m_robot_samples/1111/README.txt b/DOC/m2m_robot_samples/1111/README.txt new file mode 100644 index 0000000..3147ee3 --- /dev/null +++ b/DOC/m2m_robot_samples/1111/README.txt @@ -0,0 +1,8 @@ +Обязательно заметить в сообщениях: +- {GUID_ПЕРЕВОДА} - идентификтор перевода. Всегда меняйте его перед отправкой +- {ВАШ_ДЕПКОД} - ваш депозитарный код +- {ВАШ_ИНН} - ваш инн +- {ВАШ_ДЕПОЗИТАРНЫЙ_СЧЕТ} - депозитарный счет, с которого переводятся бумаги +- {ВАШ_РАЗДЕЛ_ДЕПОЗИТАРНОГО_СЧЕТА} - раздел депозитарного счета, с которого переводятся бумаги + +Если не заменить на ваши значение - сообщение не пройдет проверку формата. \ No newline at end of file diff --git a/DOC/m2m_robot_samples/1111/error01/config.xml b/DOC/m2m_robot_samples/1111/error01/config.xml new file mode 100644 index 0000000..ec1d4ab --- /dev/null +++ b/DOC/m2m_robot_samples/1111/error01/config.xml @@ -0,0 +1,4 @@ + + request.xml + #M2MTR + \ No newline at end of file diff --git a/DOC/m2m_robot_samples/1111/error01/request.xml b/DOC/m2m_robot_samples/1111/error01/request.xml new file mode 100644 index 0000000..e91771f --- /dev/null +++ b/DOC/m2m_robot_samples/1111/error01/request.xml @@ -0,0 +1,119 @@ + + + + {GUIF_} + 2026-04-30T01:01:01() + {_} + MC0012500000 + + + {_} + + + + + true + + + + + + 21 + 1111 + 111101 + + + + {_} + + + 7722061076 + + + + M2M2233116169101 + RU0007661625 + + RU0007661625 + + + 1 + + + + 7702165310 + + + {_} + {__} + {___} + + + SGDN + + + M2M2233116869102 + RU000A0JP5V6 + + RU000A0JP5V6 + + + 1 + + + + 7702165310 + + + {_} + {__} + {___} + + + SGDN + + + M2M2211116869103 + RU000A0JPKH7 + + RU000A0JPKH7 + + + 1 + + + + 7831000034 + + + {_} + {__} + {___} + + + SGDN + + + M2M2233116819104 + RU000A0JPGP8 + + RU000A0JPGP8 + + + 1 + + + + 7831000034 + + + {_} + {__} + {___} + + + SGDN + + + + diff --git a/DOC/m2m_robot_samples/1111/error05/config.xml b/DOC/m2m_robot_samples/1111/error05/config.xml new file mode 100644 index 0000000..ec1d4ab --- /dev/null +++ b/DOC/m2m_robot_samples/1111/error05/config.xml @@ -0,0 +1,4 @@ + + request.xml + #M2MTR + \ No newline at end of file diff --git a/DOC/m2m_robot_samples/1111/error05/request.xml b/DOC/m2m_robot_samples/1111/error05/request.xml new file mode 100644 index 0000000..c9502c9 --- /dev/null +++ b/DOC/m2m_robot_samples/1111/error05/request.xml @@ -0,0 +1,119 @@ + + + + {GUIF_} + 2026-04-30T01:01:01() + {_} + MC0012500000 + + + {_} + + + + + true + + + + + + 21 + 1111 + 111105 + + + + {_} + + + 7722061076 + + + + M2M2233116169101 + RU0007661625 + + RU0007661625 + + + 1 + + + + 7702165310 + + + {_} + {__} + {___} + + + SGDN + + + M2M2233116869102 + RU000A0JP5V6 + + RU000A0JP5V6 + + + 1 + + + + 7702165310 + + + {_} + {__} + {___} + + + SGDN + + + M2M2211116869103 + RU000A0JPKH7 + + RU000A0JPKH7 + + + 1 + + + + 7831000034 + + + {_} + {__} + {___} + + + SGDN + + + M2M2233116819104 + RU000A0JPGP8 + + RU000A0JPGP8 + + + 1 + + + + 7831000034 + + + {_} + {__} + {___} + + + SGDN + + + + diff --git a/DOC/m2m_robot_samples/2001/README.txt b/DOC/m2m_robot_samples/2001/README.txt new file mode 100644 index 0000000..3147ee3 --- /dev/null +++ b/DOC/m2m_robot_samples/2001/README.txt @@ -0,0 +1,8 @@ +Обязательно заметить в сообщениях: +- {GUID_ПЕРЕВОДА} - идентификтор перевода. Всегда меняйте его перед отправкой +- {ВАШ_ДЕПКОД} - ваш депозитарный код +- {ВАШ_ИНН} - ваш инн +- {ВАШ_ДЕПОЗИТАРНЫЙ_СЧЕТ} - депозитарный счет, с которого переводятся бумаги +- {ВАШ_РАЗДЕЛ_ДЕПОЗИТАРНОГО_СЧЕТА} - раздел депозитарного счета, с которого переводятся бумаги + +Если не заменить на ваши значение - сообщение не пройдет проверку формата. \ No newline at end of file diff --git a/DOC/m2m_robot_samples/2001/all_to_1st_depo/config.xml b/DOC/m2m_robot_samples/2001/all_to_1st_depo/config.xml new file mode 100644 index 0000000..ec1d4ab --- /dev/null +++ b/DOC/m2m_robot_samples/2001/all_to_1st_depo/config.xml @@ -0,0 +1,4 @@ + + request.xml + #M2MTR + \ No newline at end of file diff --git a/DOC/m2m_robot_samples/2001/all_to_1st_depo/request.xml b/DOC/m2m_robot_samples/2001/all_to_1st_depo/request.xml new file mode 100644 index 0000000..5f64962 --- /dev/null +++ b/DOC/m2m_robot_samples/2001/all_to_1st_depo/request.xml @@ -0,0 +1,119 @@ + + + + {GUIF_} + 2026-04-30T01:01:01() + {_} + MC0012500000 + + + {_} + + + + + true + + + + + + 21 + 2001 + 111111 + + + + {_} + + + 7722061076 + + + + M2M2233116169101 + RU0007661625 + + RU0007661625 + + + 1 + + + + 7702165310 + + + {_} + {__} + {___} + + + SGDN + + + M2M2233116869102 + RU000A0JP5V6 + + RU000A0JP5V6 + + + 1 + + + + 7702165310 + + + {_} + {__} + {___} + + + SGDN + + + M2M2211116869103 + RU000A0JPKH7 + + RU000A0JPKH7 + + + 1 + + + + 7831000034 + + + {_} + {__} + {___} + + + SGDN + + + M2M2233116819104 + RU000A0JPGP8 + + RU000A0JPGP8 + + + 1 + + + + 7831000034 + + + {_} + {__} + {___} + + + SGDN + + + + diff --git a/DOC/m2m_robot_samples/2001/mixed_1st_and_2nd_depos/config.xml b/DOC/m2m_robot_samples/2001/mixed_1st_and_2nd_depos/config.xml new file mode 100644 index 0000000..ec1d4ab --- /dev/null +++ b/DOC/m2m_robot_samples/2001/mixed_1st_and_2nd_depos/config.xml @@ -0,0 +1,4 @@ + + request.xml + #M2MTR + \ No newline at end of file diff --git a/DOC/m2m_robot_samples/2001/mixed_1st_and_2nd_depos/request.xml b/DOC/m2m_robot_samples/2001/mixed_1st_and_2nd_depos/request.xml new file mode 100644 index 0000000..22471db --- /dev/null +++ b/DOC/m2m_robot_samples/2001/mixed_1st_and_2nd_depos/request.xml @@ -0,0 +1,119 @@ + + + + {GUIF_} + 2026-04-30T01:01:01() + {_} + MC0012500000 + + + {_} + + + + + true + + + + + + 21 + 2001 + 121212 + + + + {_} + + + 7722061076 + + + + M2M2233116169101 + RU0007661625 + + RU0007661625 + + + 1 + + + + 7702165310 + + + {_} + {__} + {___} + + + SGDN + + + M2M2233116869102 + RU000A0JP5V6 + + RU000A0JP5V6 + + + 1 + + + + 7702165310 + + + {_} + {__} + {___} + + + SGDN + + + M2M2211116869103 + RU000A0JPKH7 + + RU000A0JPKH7 + + + 1 + + + + 7831000034 + + + {_} + {__} + {___} + + + SGDN + + + M2M2233116819104 + RU000A0JPGP8 + + RU000A0JPGP8 + + + 1 + + + + 7831000034 + + + {_} + {__} + {___} + + + SGDN + + + + diff --git a/DOC/m2m_robot_samples/2002/accept1st/config.xml b/DOC/m2m_robot_samples/2002/accept1st/config.xml new file mode 100644 index 0000000..e69de29 diff --git a/assets/moex-most-logo/README.md b/assets/moex-most-logo/README.md new file mode 100644 index 0000000..94bcca0 --- /dev/null +++ b/assets/moex-most-logo/README.md @@ -0,0 +1,34 @@ +# Логотип сервиса MOEX МОСТ + +Официальные ассеты и правила размещения логотипа сервиса MOEX МОСТ +(НКО АО НРД). Требование для участников сервиса при интеграции M2M +в свои интерфейсы (личный кабинет, веб-кабинет). + +Источники (10.06.2025): +- Руководство: https://www.nsd.ru/media/docs/rukovodstvo-o-razmeschenii-logotipa.pdf +- Ассеты: https://www.nsd.ru/media/docs/dep/logo-moex-most.zip +- Вопросы: moexmost-logo@nsd.ru + +## Файлы + +- `main/moex-most.{svg,png,pdf,jpg}` — основная (полноцветная, красная) версия. +- White и Add версии в исходном архиве НРД отсутствовали (только метаданные) — + при необходимости запросить у moexmost-logo@nsd.ru. + +## Правила размещения (из руководства) + +- **Наименование всегда полное: «MOEX МОСТ»** (два слова, оба обязательны). +- **Три версии**: Main (основная, на светлом фоне), White (на тёмном/цветном), + Add (дополнительная). Выбор — по фону. +- **Охранное поле**: минимальный отступ до соседних элементов = 0.5×высоты лого. +- **Минимальная высота** логосимвола — 20px. Для очень маленьких носителей + (иконка в моб. приложении) — только логосимвол на плашке (приоритет — красная, + радиус скругления 6px для плашки 24×24). +- **Web-интеграция — единообразие с окружением**: если соседние сервисы показаны + полноцветными лого — MOEX МОСТ тоже полноцветный (main); если линейными + иконками — MOEX МОСТ линейной синей иконкой с полным наименованием. + +## Где применяем у нас + +- Веб-кабинет клиента (отдельный проект) — обязательно, как участник сервиса. +- Личный кабинет / admin bj-server — где показываем канал перевода M2M. diff --git a/assets/moex-most-logo/main/moex-most.jpg b/assets/moex-most-logo/main/moex-most.jpg new file mode 100755 index 0000000000000000000000000000000000000000..802d248e4189f114b101fa80adad545502afad95 GIT binary patch literal 82857 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 literal 0 HcmV?d00001 diff --git a/assets/moex-most-logo/main/moex-most.pdf b/assets/moex-most-logo/main/moex-most.pdf new file mode 100755 index 0000000..af24ca0 --- /dev/null +++ b/assets/moex-most-logo/main/moex-most.pdf @@ -0,0 +1,348 @@ +%PDF-1.6 % +1 0 obj <>/OCGs[5 0 R]>>/Pages 3 0 R/Type/Catalog>> endobj 2 0 obj <>stream + + + + + Adobe Illustrator 29.2 (Macintosh) + 2025-01-23T13:03:26+03:00 + 2025-01-23T13:03:26+03:00 + 2025-01-23T13:03:26+03:00 + + + + 256 + 88 + JPEG + /9j/4AAQSkZJRgABAgEAAAAAAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAAAAAAAAEA AQAAAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgAWAEAAwER AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE 1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp 0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo +DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A9U4q7FXYq7FXYq7FXgf5 ya3rNn52lhtL+5t4RbwkRxTOi1INTRSBmk12SQybEvoXs5psU9KDKMSeI8wGDf4n8y/9Xa9/6SJf +asw/Gn3n5u9/I4P5kP9KHf4n8y/9Xa9/wCkiX/mrHxp95+a/kcH8yH+lDv8T+Zf+rte/wDSRL/z Vj40+8/NfyOD+ZD/AEoen/kRq2q32raol7eT3SpboUWaV5ADz6gMTmx7OnIyNm3lvarT44Y4cMRH 1HkAOj2bNs8S7FXYq+cvzN17Xbbz1q0FvqN1DCkiBIo5pEUfukOyhgBmh1eSQykAl9L7E0mKWkgZ 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 4ZBI1T2Ha3b+n1GmljhxcUq5juIPe9izavFv/9k= + + + + 1 + False + False + + 324.199892 + 109.013000 + Pixels + + + + Magenta + Yellow + + + + + + Default Swatch Group + 0 + + + + Adobe Illustrator + application/pdf + + + MOEX_МОСТ_Main color + + + proof:pdf + xmp.did:fc2d753d-6360-4cd1-a9bb-76267ec41067 + uuid:54ec4919-1a9e-6748-8bd4-618239af9862 + uuid:c3495cf3-d8eb-3c45-a9c9-080ec3185edc + + uuid:b9e9e2fd-4e3a-fb44-9c02-5e956c1ec88b + xmp.did:4be68e55-dc9e-4a06-a979-05206c001353 + uuid:c3495cf3-d8eb-3c45-a9c9-080ec3185edc + default + + + + + saved + xmp.iid:ae87f767-0a1a-4641-bba7-2b5118514c38 + 2025-01-23T11:09:47+03:00 + Adobe Illustrator 29.2 (Macintosh) + / + + + saved + xmp.iid:8462417f-f4e0-4139-a52e-dc089437bc70 + 2025-01-23T11:26:54+03:00 + Adobe Illustrator 29.2 (Macintosh) + / + + + converted + from application/postscript to application/vnd.adobe.illustrator + + + saved + xmp.iid:cc27364a-4203-4fa0-bd63-a0aed7df92c2 + 2025-01-23T13:03:06+03:00 + Adobe Illustrator 29.2 (Macintosh) + / + + + saved + xmp.iid:fc2d753d-6360-4cd1-a9bb-76267ec41067 + 2025-01-23T13:03:24+03:00 + Adobe Illustrator 29.2 (Macintosh) + / + + + + Adobe PDF library 17.00 + + + + + + + + + + + + + + + + + + + + + + + + + endstream endobj 3 0 obj <> endobj 7 0 obj <>/Properties<>>>/Thumb 10 0 R/TrimBox[0.0 0.0 324.2 109.013]/Type/Page/PieceInfo<>>> endobj 8 0 obj <>stream +HlVI9 @ER1y@bllpPK. + +W5_(zH 8؟սo_;HS> ޜBwǙTs(%;$:yUɶ![ڨd*0POz a0XGJ͜@ "K(rD4gsK`>e:{B(Gx}ļl'8Nu]ݑD-R 0~"'UtjBsY}0\vxmXOfPUuH(QsE1(B11PL~z2 W)-Ep;O`q:QQIRl.Pium8OPOC]wʣv>.h1I$P'uuCm  j@B!#G*VDőb\ew7T*tFaY"um#xyZI1/61ZUpnah vAAo.i4L#jX{f +rZk`sƉP!"W6nJCh&67(ą{iԚ% Z֑N)vM#&F-X1n`:3y\p762筻XF6zMAfAM3iY{'lq1z+5ģQpfP||9B25Z1">.{/䉠d`'g4`0p~d֣jK|Ñ'6e}\'|~v&= endstream endobj 10 0 obj <>stream +8;V/Fd1*l6#R0t[kOIXV[n\HH,*A\g_q<.hO endstream endobj 11 0 obj <> endobj 13 0 obj <> endobj 14 0 obj <>stream +%!PS-Adobe-3.0 %%Creator: Adobe Illustrator(R) 24.0 %%AI8_CreatorVersion: 29.2.1 %%For: (Evgeniya Zelautdinova) () %%Title: (MOEX_МОСТ_Main color.ai) %%CreationDate: 1/23/25 13:03 %%Canvassize: 16383 %%BoundingBox: 26 -108 319 -9 %%HiResBoundingBox: 26.4301984518825 -107.492899999999 318.183391962011 -9.42722999999933 %%DocumentProcessColors: Magenta Yellow %AI5_FileFormat 14.0 %AI12_BuildNumber: 116 %AI3_ColorUsage: Color %AI7_ImageSettings: 0 %%RGBProcessColor: 0 0 0 ([Registration]) %AI3_Cropmarks: 10.2619999999997 -112.9666 334.461891550782 -3.95355999999993 %AI3_TemplateBox: 145.5 48.1062999999995 145.5 48.1062999999995 %AI3_TileBox: -223.63805422461 -364.4601 568.36194577539 247.5399 %AI3_DocumentPreview: None %AI5_ArtSize: 14400 14400 %AI5_RulerUnits: 6 %AI24_LargeCanvasScale: 1 %AI9_ColorModel: 1 %AI5_ArtFlags: 0 0 0 1 0 0 1 0 0 %AI5_TargetResolution: 800 %AI5_NumLayers: 1 %AI17_Begin_Content_if_version_gt:24 4 %AI10_OpenToVie: 0.477903812848126 23.9052077639226 5.31251070480716 0 8209.01875275996 8236.97163761074 1716 984 18 1 0 6 67 0 0 0 1 1 0 1 1 0 1 %AI17_Alternate_Content %AI9_OpenToView: 0.477903812848126 23.9052077639226 5.31251070480716 1716 984 18 1 0 6 67 0 0 0 1 1 0 1 1 0 1 %AI17_End_Versioned_Content %AI5_OpenViewLayers: 7 %AI17_Begin_Content_if_version_gt:24 4 %AI17_Alternate_Content %AI17_End_Versioned_Content %%PageOrigin:-152 -377.393700000001 %AI7_GridSettings: 72 8 72 8 1 0 0.800000011920929 0.800000011920929 0.800000011920929 0.899999976158142 0.899999976158142 0.899999976158142 %AI9_Flatten: 1 %AI12_CMSettings: 00.MS %%EndComments endstream endobj 15 0 obj <>stream +%AI24_ZStandard_Data(/X4  +撓;Ed"p$1d#Imqd?pH(, l r rm1gv0nkjaU[<7up$+U!ڿ6l~[x㲞e=a?Rn{_1ɼ}^6e[p?#q >eqe]֔ x|?_"iC3?{8>m) æ>|j!G0vfQ|یN @ȴ]vzHaCm4Bv֐iP24Ɔ 2ݸv<̑[e})*,0H[ܙILg{1MN/ؓyQ ^ž&C)jb5& 똤-zglg{ԣ*"hH(E.nQZT4D342$10p:LA4 AbԠ_oS{~v}b;ؿ;ﺫ<(7yKNr;\sՍs&o7{Tԥ&5G-PԟklSPf3a U-}? 2E}l?`'c]u2|}uQoZ&4u7۲ϾzVcSٯj]=_ο\5ܢȦ6 גN3;W̅a Þl%}?rܺU@']?M[Nyt12YWe--2mOd1| CyOk__Vr7S5 +v6L_j'M h:5'~YW$'5'؍Q_I;;Q쏌y[U l(OB~fg$[ p7Yŝ}ޡ-]$ ǭ;R1YE=~Zw`rNP55u|5)_3oAIoKa_qȬ" ZO۪I}l* ˞3{g=u$(XCZ ĻU͐>A}?߻/י_ $;؉";}ad;8@=?$CY7nyO]d|5nr}gaywYmg,p۪=04R=Н)K=BуDQł&T"r;I 5 <ChhapjfnEEA,?By dܰODqj-4i˘@ S!qܗZ;CE MeI8Ӱrht ͠;qFmrf3CHNDYe+%2;a [bi!=E͂A2.}ߣF((&R@_Y~Jy"Hu0a<G s5jHM@;O Ja<,&WH8nD(CsȕQCS@֝8Ұrh.OXжX?0"K؏r1p<jcծ ʾ +^n42Ʃ}^MҀbpJ݈dYR N +w晾PtDMR8yHNaMPKIQXj +@'w NPҎ'vj1A %sԝ)U*uw@ xkCCCց8Kڪ]942 $Qx*pr0q iQ\m + Mt|f!4axrCl8Y( RTc6C%P¡tGaHh G7TH @iI`PP¡J+4=h‰%Pm#(QpXNe -,{ <zДj޳ӛNh +_X]AoF +ꬠ<:Uyy51I`M[^*YdW_?2M_˾kd6[^rvj8fm6?ns^pmg/g//˟Sf2yMe/v+g_ YWs?yٯu&e]Sܲ7^{l~ۈw](Νew؝Ǩ*v?9)8~@ѬSohxx8~V7v&A8.ĺz~}UܺXa. 8{:vPŬCSv6 mݔGuzϚ:VKuʻS?vQL/ߔx5*§Nٸ(v%wr<{bpܺ.&m病O^~b a?.&5?좓?V:)|@m :Q3 #d],ZW3ܵN'K`cTܚ:yag@f[-CNFw=;4oAlYʌ.2e,Y1Oo&7#4G{:|D|xȻg5n,rne{\])e(DlN<̿DV#Yׯ,ÞR| vrwP6 1Cݍ䪀L_pԺ{[;_Ey v-x_'?;$-ؙEX2 2LgvuȳxĶٙEoףQ@,< +.hBjF `3IZAn$Yev~ِ?gmÔό/Rލ|zKb93_\GoN xjQXj~~-o.{3'ي;,_zB&񫓉ܢ[Z_yap ByϝϘ^N^ !_番Cwv&{6}!a]lIxLd1q}&]ϞI..R.eٓKt̞\dV[fԒHK>p\19,C&SQ񊪢j|Һ [UQʀc(}zʐ:D9JqްM$G6 +Q'r%-{6g(gQ,qZjJ7A?6,Հ|R߰bE)I(n`w)}؇Q[Vp)R"AcO(A+ ގ | ukڬN(AP@J}~>IםІ1D2Ę3dad1D>>o 6ņsOu8@eF2.T١MfMdf%EV7 C [QdʰI (r}Xe]ܤ+)Qӆda ⟍ eec!O +y6mmqZQI +Tƿmy` W `$G>a:N l" K:ftG Xȑ B) F2W;f({3YTrI!BNeAZFo!/Ф a"FZ"b|,i7 +.w0yFtMr"]߸ %#H [Gy oy/be9?Ȃ/4p13 7մ[e P+I|2C -F AFho*#rWXp~T?6 +H$P.~z(a"h=7. cPQ,,IJnHN&(,'} Jyz?ČH0+<~V> X7쓀XuԼrJFԃaG#Q:1Ua@i+t!@V=ȚVuHF`!7u\ J4P6& +_g:$W,(J@vxA! +BP8QQ: 5>}c`SdJˌt, 1p< HK`BؘXO+l\6lWCnWɆy#luPLRɪƆi-wuSd`dUeJ9 +Uf"a4$S^ί3=Jc"WSQP2́RR°RDTM?!u[es$HaOƌSZ.@[PV5Va)<3I`@Bdq{:h&^X֧ +'P88q%ᴌ{hƆܡ݉(݉ g҂bJ#H偤xiVy L˃8w@ oNH" 0U~(H#0n{l{R^ nS 9[.m˂^vSIG $g\)" @*:y4t* =;'R#j& +)@Pۓ2{&=SFc!REw $d,@H C$p*232(I58JZ"5Ò1 stF\:Ny ,!)K 6 +04(UIMDiN2:M[P u +M3K\iN +CGeg`TO?Z+P6O% ݉FC-Ccxa6کrVН8MD^ciɡiE(9r`hKs& +v5LH\q;BFTϤ%o` k6 Yә^Bn0l$JD +.V5nR$]JFת97ltڥ-Ӱrh3! +/ DLEͬކ=jϟ@HSsM;!L"H^pƆFoRff[ÅR MI'8^ +,,qA!N8\N#ݖ!jԎiL{&ˎ_)*ıc.SȂ;@;; }k$5uI"* + +Y!Y`0b*GՒV5;E8چiօLGmHt$0"+y@_l$pq*mZ $҂@/~`Ia*x̆ +J,*b$W>Wd:/kCH :xE;$(L+ +{̅` jz=jhH{dB +RU'Fo/&1,8xĦpnW:M; C9'>x<@iv!:/JOMb&^?H׭ͺ&|g11]ZKhi`야- - bA@)p|\2*+@&Y+jrSܔ2p)CSmP$6gJlZP¡$}I>2gR2Cvc%kIB}u@ ?a#7 tc"9".(}9Qȑ."4Js +! +F6^! +%cmSR5o BcCH Jc +ސ>Wa`18EEzA[=y!=Ƈ8 `2P +gs Bji!pv`'z29& +=;́́́;]#huB8Ύݻcpi0ʆi _}IP`ݗ*%^e]W)bRWDwpbQpTNl^}afq)Ie"B*A6BW\Y92.p圯@K \ǃfkǚ0nWe,|K}&ױ[ #*cO@ce=X=} @GPA$"FXhFGWz @*Zp;J +&U;B;xBYIcKS iV#KH) g/d(tB!ɷ~' C8;@ hSHi"s'YXA+lSy<Q臃k0J}nuomf1L.R.{ri->@. @L@4lGˤtEőB&%LfIE+rd+j=#kE-8k% + j6 }g䯋pҪoú}+zkh/lN=:,\oϞ=m6^Lt?$x?Cώ}O8Cm9UAc-7KcO.+}zg0 V,@ph˿Q!O <3$V ]teyΪBVˈуޭQ}7.2ٴx סΐRf㵴GlE՞vTmV=)d[.!_V[UVp҂ :xƝu`vwOE2}LWa~5Ѽ!Yɼ'b䭉s#c$CQ~= ONy= 0 +<#H"PR}GwNEWƜ^hZ3a-"MU{R#z]58`}؈XUш.Z&P{B+p*8G ?;ktroyqrϐox0FrϡcӺJ<Tz,H -?`O3Xr_' əǞĂ>,~4zG+2O_ݞ +EJgr&.! +$xaK;`j?pJ|1S=vEC_ p9enC]%JzRڅp}"~}}=4!@DžcjUM .e.gg;)8e숉 +&TGOQHTO +5_'M:40%LK뙞{N`Ք5O]l.b*{8\Kel92:͇5LC<y1Ӡ!]ze&(8543\Hw JuX.~ 3;x{VKIVaT9AHe\,!i]1%9={w] *I:q= +W[2f ]XB}^FBD8"b Z w31X)+N7tZQb8XzoK$3C!Q :qu`/cj 0[j$}%=c GCfșČ ?gjtU>K~ x 1|1xA55_Sh&Ӗis{Bd*f:*.O鮟gjQV1&eqT.3jD?vۧrLw+׬$Zd%/y:J⤜a;6%ǀN¾hVy +lؐ1?)|1x:w>_Χ'PE9rtBSxBaЎDxPKTh Q܋| rj^+׳"*90h +S*}"l\kh[-p-e9 ZU"O0:eWb!5$GyXFB[YJAPx(L(2:= O#D87~v ~r81C'#igPE_><]EAΎk +g?H'p;m5z&MYedI4>y0SjV_,ߓ@┛,v SnQ^(<% **oNn_dE@,PFNR]iGѦvц9 FӁNä3BoW[B1bMx B«yI.葯 $np\XR5 0l4#q-3o# +bT(<AW[1h#lqC`GgvH̡Gp.}ƍ`NTBu ``~Ȁ4]ήLfȦv4Gpqd@bÁyJQ鴅KOJ8 Jzdt #pt(}b]zr aАs"y{J^Ih^)#mҙ)5Fƪy2'3ʪ ԅP-0Ǖ2U-:3d;B5xPj[m#qT ^Z]8տ[WK ПXuO8S= 9S\zۓ`!YYIGIG@8LF Cݮc7ɷ4YBf)^ 4qj^;݊p.lbALMxWK$ /ŵEWl0*D'ĢF{2 w/ǀ18Gx2\b7Cf2!-8o݂FS}a])Z LMGo}(95/$NHOA8JG--n#E"`O7./UCȢZ˖wZn!vE]r'MS߹QdO{7(6ێ0Z(y};1A(%k5 )SO:3+-AOFlYto`f Q +J&Bwudƈ%b6,F͕rZW-bE`#>G;ܣu'Rh*Q?0>&?>`yHn:=Ci?QR# "3Tl~ٞS8B28p6M$;ːPxt;22Ծ*d;' `k| xbѡj.zٳGnL,iš#+"UXm%'8A/*6c r-Z6*'X2_~]3S'y(tPTԥJ_ PWHʵF'*LK7qԵ 59@] "OBcWI{~$@]E4RgJ "G7/4QQ{.`9N{0vǢ;8[$\_k 2l ZЍޓ+_] "bӥ7݊TXeؼmf|3#ooDqpZ12ږ㜐|NW6R_> y!۲wЅdM$%mGN )pIoܜϪe !4/7N(TP0|ŽYj$f@Y.Y$rꆞ[QK1W~xJbIa.N-,J]vTq-]:ue$ZSW ,6%*uY`zv\l7j -}=..WB pY !?S\G;{%]%3,qgi]%\C_Wg>a vY +N-iF2D%-RE F ۏj #ZPsƩO8Si;^e)":oHqM[OnmB !ƭ,\CIo5Ii_A*5:xo$z.>s+btMq_xkWX +|ܥ|HrhXA)J+HMg7_ M"n*{_JX'+ceVc Ż/kF@LU/>?3f9oatG!ڿ77KɄeyc3qu({c<Ȭn0\=JZey2eC :9n5n${7$z +U(YRޚO`Cs xD!sl=UHҼ(C>+{7aNThZ:np7p5fL|"MѰbxȜ`A6@Иs{g1X~}+îW]=pa)*Ű(r+,ZAO" -arknR[ke(=dnJǢ +*1n_5E@-;3X[I3aD?,(eQ[D [PE͜r_czJL?nVq*GjA숴P:DC/EΌnU NHN wCPBnG }RѳXWBlA Ya0*_=-L7̀@wL|29vn|Oy3:[>{Kh#ܥ؃@T"qZ&m!)*zeH .+`g52 qbqs Ok#c-.v"%{{Xyߑ-0ީ Zχ}O'uwhm@~VD^<|B D{ntBc; T8tM:PϒRY< ~%`|1cbJcF-ZW`z*kU Bnyy{H05T{BkR# )/nQs/ī&*ãe)hx5-_U\ms7!qhգ CAdU3 \+<&hg28ۄƍmItMm&.V 2HV"n~3 Ё,Xnoh#m5JEE'@i7k+"'=]!04bR'4I2. xsBKF#;w5)_ j\'XGY5IR9I? +QŊݰ۟g!Eڥ(,WH7T/(HN!HDAWP z@  Xi;pC8Րr |eueq-J<[۟%  HۂE\0wgG.X㸺\6I>-=iyov̫ٙ8U喹ᚁhU'):¼ C^p&2ۺjLOνGy"n t)sw2HZüa;{aQB z\ړ~,qA@HV0$h0?%e&z4sXX҃Yu=4+ct^eוAVj H["sCĿ6/;tl(`)/h`h+hbmyhQuC>ֽ:{\ۦ v ]We$L +A)&:[]'{>\ Qg]Db}dJ[SV[TgOJ@{ᴉ4Bmd`Ryc:>Q#-/qIu2[EM&U`<+ȳMȧi׋#/ڀK9X^m}ma0rgԙ`^O)S +źdmkF$b<,-ܜ\^ơM/+447Y?sC3$8vL5+4͇2a ̦M3s0omB3]!}]{ ͡L H agֽ| qWY>sF.e~]L>syQ}rX,4#'fxB"CsuwC3BYP?sy a*ŗFm}&4 g]wR5BNc?sSC<#Їf$ UXlB3*OX\}015fqNn3i/syU[L1!3r6䖝 2UAȀOrt+2~s >˱Y!DLIY5.v5{"[xb) is^L1lG:PĂX']*N031eatnL̇ EӒ"vyrO7+c׏!a/#QgD`!hq)h-v"g8[xIr:9Ȅ$~X@GqM64Ĕ40ՈҝWx.)_1B&DXGsyl!Qdy[0$uufG$m|-cH yGwv8BfV}'sgDcluOŪ]dȗ"06+_ }!,o%Sd(,rLe0&h]@}xSTp+ou8MVА +eݔY-iTܛGeQ h;<`I +o<1?2g9:&lN/]n0b0GެJdz4;>;qnx"O0(']RP<`ޙ|m/Qh 'd}'f+=3zvi}q~%y3{]Wn"Ln,-u8bXOS8YOQ *|Qd^vLEi`#Pl5 TFbQKCN(s,-]6b +RSW@X|b91Ut`e0e#LDO9Dd?WOV9=3r +؝'~ cgԖ'tjHf`],1k"@|@#Z⮰1RT?Sf#99(1x\-Xx5#5zF#bÞ4<O9]ԈT*5T7&P}Brx[ŻtLU gG;A2Y-S`6"jĝ8=7ix&ckI+Rk'p!Dm=IQ K_|\v{@Jٴ` @ d=SI/jgGk.Hawx$Sغ w7wK=&P#$D̐AC }l1VK +Jm}@gFapkQYR 2g?hQ7Bx0rbzF v[{'˅yqDY]~UK,j/3&%';ꗇwܼ8LLcڽsd{$JERgCVVK8\k>Sm:B/KaY)yKjd 1EI\%l Ȇ؆-T3ML<%- GZYK|6j$N5X +|; y1,I{` ˄(W#2 \g*L9C_74gQN?QP] !ۜ~bA]Ci/6 Rܛ\$sobVx7T8 $]䃴SLoF^YZ% < +O!ǧ+*,˩ &alElHcvx y_O𣎪 E- +NQZ`| G@[`Ĵ9 pAyؼ+Y6>9*D:G,3|(.s  ld<3~8Y &X!IhbmuPh]_~{yb$/Mb)nNQU.1 Y|F XIVøs )Bojl}\F0ͱR>-4#yy(L%t U0`5Hm- q_g5r_.(Z&IUKN` ,P4yޜ9TzrCla0nl'Vx喰;o*6^vDX[d(h r &G]zSNr* jse;&oNJx#j;4OQeū +|Q?1 5T!qOI +Q0 9R21J*s=S;_^!}x +}9'JZ vTz/3a#abt<` b#wrJb[.9:\6KFR95iem@šB~m94Zp8:1u2+w-`ǵ3W_*V(?zEdݲzJY.qz j/{Lc`3-dHy=PJx6]~昨B |I0<> nxL=f`[pw +v jR= 8J14 cI +=,= =9zy» +s%R-yKyJLrfRL Ps HQB*b9 9A nz PLGKkN~¬c53׈Vs[knAsſh2*Mq er֔/#gxJideSRG>FQGj7qjqyGE̽[Q*|ݣFAiE(^Ϡ4DB +sH1W?FprU+1buؑ0]TG z[YkeBR/xY%.gbB|2ğH,Hxd2PX4>9i2!ֳ2~ \ }{ELϭ"V@ʫD#+`RhßK0c~eaL>M2 Y\H#Imkĉ"K~o0B7HtւSҪ3ĝ\?uN^)Jvs+Yr~Irԯ'tHR\jJ({pTFӔ<_aBBSJp *n&i؉Sbk[qIk/ ]}T`8$JR<]`PR: 8Hs>QЕM^'4R#< |"O ~FcUtX>88f7ˮ%+).q<-/cT %q3*bqqG@mID\/mѕbM Bddz%[\nD~[Wgȧ?jTbΣm +!jޒH (0ްm 4Ancwk‰_0 נ|pNZ<vjQolIqW.Wyy"KOr¡g^v"[-de.N;Dv1LI'k$>YjV\GeIKqiS*AZ +$d 4h3C ެ= 3xkeLQW4v0ZdmQk +_FH|`/ +Z`n‘Ly.I{`<(|)B._9L#˹⊵i}dnC2 aWяF>Q6CYevt(޸t=o,;IL@dW? XF~il!1a=\iG¦m+ZH~5.3gŵm&[`EJ 'd2bO_E3W$j)@pTB;{bɥ~u% R-qV_zV+[($dW;w;@†w63 @ r%ɄҦ+[w@-*Ʉd"Pco +6pK&q"TZ.h2]`ZM)[Vi/[3*`aIbua{0},ɔf 7 +b--;"P?ѫfc ln2DM=?uj}jNx4(TڴݝZeK9N*jh䔪o?UפEƒe Z$'*/`]W-ʄX5rbOlW@1k@OIڀtJ|8<ڙbiT5 }v*#6U4\Gi"W9I)E[GMZKjn\· 0u$oᑮݱmD"d/O7/f}%.D܊.m0 +j^^hɗLl(x qaVG@LLf<%yʻ?֠SuQ4 AFl y_ڋ-yई.&iV`Șo={7c 1,hJ Uf 0Ny!û YJC &+ÛaݭA<F`ߣ i f'H+8ۧl)rء*}H/.O[95S@⫲V]OECeڍxXYb+MprUdIadnd'3!MNhN}=}[aFqoK-,e|6GQEXAg9B|o+r+aWSn'htWVv- SןO0$媑~9E8= |41Cщfc І53&Ȑ1g + +Q!A90!1$O락иx:A>#Y=B;ڀiK-,swE"h.ATqFĬHXڣ!GcaDEOT![Q΄"AH@f|<9$` K E U:h߱]ƝcR%~e~m4h_}kapA7\ZQDچTz BA$xmp{"ڝ+Xvh\D~'4~lZcS 'iLb9. =ۅ}1Cp?ոb^LÜky, {w.qty4 +˺Fo:r<D_S@lVLv+ƿ , /:>姙0+?L/R]l>xe޻4؅w&:c9˭5|_#G?CAC>h \k"z7 >$DyULAi)^SRf=VVj::xIttoNRT0y$!__: FE8,"qAD>˺ +!Xna)ﱱKuB`)M2.'An,% +z^nL1pqb |Xx'oܦAz ]v#|e Hil>/t`Ng3L޿{"6]H)yUa"EO-ͽkUoKs*G2:Xb@&zR~DԾnȥHTuQE\u/sseq]n MO}Y +x6o0꟬sp'IJ+Vx׹cR`vܒQ\X ʠY?!VJ*$'H17 AqMMF9aPz:]gl3;cb 5PuE:YBBՋjإc*N>+z)- X~Tsp'tVv`cOn""rwvpq:o𣛁ǒn.D(1)sv-r&'בd:|n6=A٢fub8 ƂbMFljrP;q'jqKue?Qsau:so> :lQnȍaDVv%gqqQiUeaiy i:`J]uH}] "2\Ŵ,BM:3KDlJH^: ͔P8 2 ȭ.~pLW]ĝOST6&w`Qufe@TU-^pR#%yǼ:šfzǰn㓬xV?(GA/&S`x?^Z@AـɏXNM[:ɇjh!d +' >G :2T$VZ +6h`l }3xx+ o|_iw8?1ljIXvjv({=X~ +vb.ts^4N q0W|Z.(bHzA!{A +Pӭve'US>} 8&__QyB'u~IYȃmy6q_[Z,#ܣ:<[Kg>o( 8l+q,L7Ο FOyʼn»[ș47(jKl`1l!`(u6/潖 hVܢZLkoO'`2.ɇ ] 񈸎f"4,ZDyAU὜|pٗܢkhM nHNe`a[4 +d4[{%agKJs{zPjzOT={3w|+lӒw `[;|GIMkA؂E0TUxB7?IKי ^YkR7CQ8;_NUҗJswLJ:4wH?thp"`~CgN\\15 ʛb޽ӳbƼ#07Rb/]sK_HɍLIO"UHs(u t6f+MR=ۃ&W1jYL5u@]ZAgu9@^+UN>lߡPWг䄎S6,#hTC<m׷!^"3CA#;hg Ɩ^`'ˑVU3M(ߎϙ &}&=BwGaES2/-#H$Ov )}OJh7Zb@:CĶ-~/R]'rߦϚRM sQiUT89B]k隰0A ;|K%H H-sb>-%l9sz-n::NDh2f"*ډpG_AblqXVJrxL1.'C]ȣcnV}k "-6.=Ū^-BtC`,+-#Goày tQĢcOA-)BaF>oKP'66"y:"P>IRrܣ߄>lIžSJsuFxK𕚼LL^%w?Iin">BhRlVSx5OꙀJ})a bty:?J@y_i%4&1NG"ڕHiѥ1Y,(aLuJqJ+an?)=.Yd?Կ!w+u~郸^Jt@)ilp߹41N8G_"\uGҢ|5^JsvڗB)45m +LXS~tz$Y:Gb@TΒ( %!xS-鋯W@k@ؖ** pnw{o]A%/ }d)ͻAKy:XQi +*/*ӣe3S x L彳FT GAYYRU}K\GB9i%CՍ! @g~:mh ,9X/ \T}Y +eh,'rj†jh:Q u +[pTop'lP"Epڜ(UTax̢|mAyuXH;98؆҇Gy T9bۣ )ɧsR):ǁ[ e(HO3 ֕ H>FYg/"qGtEUuפp +>aP[uZ\8!tI4U:(g2nꬳ>iTIp +% 甖d5E*!Uyd1fdcȃr>*2$&& aH `@ +CYCI& #Тp8TOЉ/.q2r +: \ƴZ$b q\M=O@)(& Gj='Se Qş6RI*^˧iCz6-2vd߿jt@ 4E~߳U𖚾|‡%.v!B-pl#bfӊS\GC\7Ǵo;|_bG9]UG|覦 LB*bjS'kPOI1L#kQS9^+oUea\mB'thLRL@BuJ}J`Ǭw7Y:S'U3?N1R*]^q M&tXg MPdo]1Uwe5׺MTFpYiy!A;TwOG}H~nm}h=@mhwn2fmE8qcFb +J$EL$^ @yXd7Fr''UdGg=+dw8!x?CtBʙ^XV@I&Co!pxY)֌ Wfĥ;sDu̔pƚY$$4݅ו(p:#RPj|8ls)$v /օ|PVSO 7&k˦UD7:fl$'!2hDl2D`hI&WWxMmRެ|.WW*avUDF^5iV5m]2ZwUEy )g6:(Bٞ ԭ :?`'$"i vCgVy H=hڏűeYRXo+**j +PhTMEkUU=$`[w4gRbXh<BGپ#T%/7۶.mɴAւΓ-Z`U0騭50̕?u +:4J;Rm%"yW_yӾSE#O}@}Z$E ;x}{SU8izwyd]݆nTBzߩ^S;Xj{;վ{uT ީj5 O5Om;UTKN}jD #ewy53N8ߦ-AXjdu0s02wPNJ$ӷ>Eb]87V&\koXݩ[e@ \YqKI&xR.8B{-3 L MVHNZ1:#ߩJ ?/m{s;`WqxSm/ca=|v)(Mj 9+B FNTJ# >SeTp5// +vS;UL,8@>A|:G滴'P,.Sz%ƒnITyњ `ܴ~7AVs;h_-aw:,&+DSUsВk)s`2,#4S%TMTڢkm fU,RL Lju;TzRZfީʔ\--{If Z T3pjT4NlPWT!qЖ4^N0ghh>u>m"ȾN(d"ð$sq*zg۵HSdM( `YG`ru^I=3*O*WP[HCS-@Ebùٖ飨#{_w<)@ ϞTҊh +9#n'4Ҵ {6T*wm;GQndD_Rmb(w}q{1~J++OҤ下++XߞbEh^թٗ@7%Ae0N;e@;){:d +FK4T]a*pjPaEןQn9L"ZIY[`BRGT&6.$O7[B̝jNZ'jk)ClHc۲ jpF[T7SU%Ӵ )lj@Pgsk#Owx}t-g-eϮA CULDuec +٬6}݌4hWG>5Y (y!*Pk S\s|5jc7dT[ݺ0]zCM^7&>-fGOw(^aVM.DzPn)!.[V/:~r;4+i1aðP3dR*ݲzƤIⳜ(4(#K/}|u^dbLJLyܹtH{D +c:/&.BuE㐡C ix%ETl^\J^C-ㅗ +3sCDnp8+B=20^ poZ> 1Z R7Xm ^z"@Cͼ6eL|FTMJdYp#8,Sd1~d]ld3(7x\ uָo 3Pk 1yo1*6L;9zQ aWčWuQEOhyBtK݌*ʼ* FcblF0]ѵ}9X7~Hdӈ8(GK: ͧx(9>1FLz*)a҃N U, \ rп8%4%gp{X9;b<{=#'ND PR W;T"omu./ +L0ϟ#n'З + ( +&# (a~=" +0Du5ZLpWXיZVTtdVs0|$g -FNz7!-B=$ش?'5`[7ZQh| +|%KH5 ؚf:'L /-zĔ]j}:q䫿eJßcA"( B [ŭ19Z05*Lg; <9lilrJ򑄽3Z/[>[$vLLxkCUn6,fz]23Ub޵czFJ՘["v ptجKrYxTY4W-/H 7u2%YXt5\㝼+ޮ<՘q_^, +I °pƔQYDsEKw=y/qdEPaF#ۂȵcgJS%Ycjmkq)WclHļ6ϩM.Lq|rVԏe +y>,--^>cyfZ(rjq3v*vxiE2eqp q~뙲DF;e`e!1~du٢FK-n*%9S}ZL,C#:-¥%sFqT:c&2eYOIߔd0)kjQ-8%dS#s]Q: +K%kᗘ]K8t+G:1Ivاj`Hۖ#Em"D0K(f:>DH;mJUU,fmoՕ< ٙh_pcX}ș@153r|.- cbu@,t2Q3~⟱,ߧ+g+43WA\7 TlK|xfXx]R_Aɥ'&NB-T<nsf*[[R [j!5s%]9 +r+*VĖkJyT3pwYEA__~r+:&-)Uf.bP42_T͛ l8)uR*@=` \!ЁH"S?Gm騉4e|Vj /xiaBkz)wr +B6q-`փ=~gCŦICV% uhSkݲ4 tYg$ؙ' t5Xc2Lh3z9 Sz%"+.@[w>xSv5;ptX-l?"b%,u˝;b!ASɯAt)kшE@xg*/spP|Rabn11M??dGsF<3&(.xn@x 7<Ԛȴ +2ơ#cD2l3'/l7 uG@ܕgA!8$h1w[A?ZkGYK&jU݄p89%5f@[8blt4wȖ'<98 +xN ~}8x냠M='<n>'8eC@jΨՍX4 ȗǜ?di(P&>֍OXܴ6\Vw/߬nEPG\X7B,֗MaupÖʟl{oѺe_{xj4Ʊ!CT-}[la4=vg@KIԤ)`Ѭ441>WN80C~hbֱSh.J VS_y_Me Pᴾ&9 0c@y'ϴEEL c&(e|.>"yEG9ܯ˼>CAi^tǹ?GߗWZ9ZPLN1^!Sl)gKɭ2 l]5(S#CF_F< (6*z8NTIGg|Tj`q|Ʋ¹  a'jhꡌ& y!&J5?+G&S2H\Z u0[">=q^!R9ܵJ,m. /"Nj '$16;|edUJ=3Jlޣϰ[59AْfP?WnrV»{ Ug3:K\'Uz+3KYI{pO>.%Yw8.@['Sf'^ȍyk,U7ԩD,qhvw@cas~<4kd]k“u?p(4+VмkoZ[vWX>YZl7"D*\~ȝ1 43E?w,tGµrt[)w:@9vvx? B3^Ϭow8-pY?gМZV3KzB34Kf).jNvs._|unϗ]0܇3STxp(Lwje39HFʯoxA?רmȵPePI?<^)<{ww@;9;eʱHY܈՗D<8"2ې|Ӊ9~dB[ ̒.6Wx):N6LUʋlCa pye@+PȺJDt @hp /E|J䀹'>Vaĕ: +@4WHPt]_f$Q)!ebѸvݦ(6bY)q ˕ )p) %S_W茂UX.IPNeܝL΀j J`2W';nQbW4 [mߦ0{f0$a4۾\N&<%d. e9}WS QK+NH$hQTLЬ "aĂAoVyXφ-=&kX>KQn&,NRZMQȵ y@BVԿhhO%0K4C|I(l*kSMԡHO_&1v$Z?tڤG26K`Ynf}Ats\hKrSsDrU'qAr@:r-h4Hs!Ģe"R߂~BM"70Gϓ;6?o5w[Viy2IoX@ 1S]ON}#QRlbw22b<{OӡC +Pϰ./i.@FeVE+(%EX)<UG'+K#ظ%ޞ ǗEqHs)-~aihOl ~*[Um:b3 w6 D!9ͥUVs0ŀ"hĒTQ#UdZa?nx[Xnw4@$bI!,Pk4A]G {bf#*}:N>ɓJzqUuF.J֮-9̠3J^D ;[X僙&Eྚoˏ5aGKzF)}&ppU#!S,K!_wLئs9}87![g nE@kg+XV,TlEnELJŠ>m G@lC"^lEvnE*@؊\\l94]nţ يqt+cBl)nE,Pb+ʝϭG|fEa+&ā[Y>[Cn[e+f} TOqmlEĽ}RD4R>C/t85&on27ey~ +v,wgz"PdS YCrˑQ@s- 8y7 9e3,ÉbkN7L=Bտ {8gmZĄ x+}TVA#aV,GhZqxytȞKDޖmkɈYes 3*lwv·w6);{ bz0 + =9e>[-6ESjྞ~c|x Ⱦ>'ʸު-);y(3[?G+ X1؇at{"ZFyKnFDRz&2DG-*EZHH&ZDMV0 aMG6ǯQt1B`zorEaE :̯w|ev{<9htDQBc>1G.nwFӲ/n+(ӈN1兮 Xrɳ\td +#M3> +o#6SWaBh[ҁPu XS$$%yiiY;(.Xr@C$C'*͜/uXI {g|oۢV^)1/C᳦H&,9q36P9 СK)L^9( /fT5u@f*ig_oQߍŁڠ& $e]l!}ƀGY52z$@M +TJcSeNݾ] +XA`YXq`[>,w ]rv~ ϨK&eC,F/7l'tn뾃ǥHܕ,4["[t_.2l,s&w'xkf35i0Z3RQ6mIn*R)o EٝY5( 3iL#4Iׯ N1 4D.X{4iI/䀮I=W0 i+IFBN" u0jǯH^xDnwm+燚ϋ:!&KRwiJ9 "{c+D`/(Ne^37PI 5gˌ Bdfj:M d&2dY ir]I][=[1))uIpeq#@:h~xIt_ [DsLDvgH7}6pcV.+ К,I,!YblQX_9:H[CDȱYCc[Fdäx< R ,δe$2ObG|?ejBȹz!$jRZ+?B‡vF9F $XVF7*YvH#}'״r:*|u¨GbYMslAD24MD!iV14暸'lLpHK?;C8 [Yw<$fp\S\Z~ +{딕yZd̠.@7Eqj6xeO47 Ij-QP5c?涤)eZCO>)4d4 I}aPr ?~dƓFq\2E:Gc<0R>aheJ @ f{u b+Ε,PTeF/\~ S41#9ge5Bp@^ `j]GrSè[m]X׮2 I'HO` mLbĬnw0کsk;D$~d+undQP\X( (, H(IyC\Uas&ݝHֶfslmPhy|>=;uWX9s+v|*% +<$LFK Sh+QSyI<IU{#C>-k>ia)2!6I` "TP1&1!@G*#JJ +t7H~F֩@6s-l5ς Si2;77 < +1OvwX9=~][Ā|D3 fbn"ɺ7O]t9`;l=j'yvs'i'D'юJzvNh_L 9|Cmmuw"?(~kIZs{gG|¤^h= x}6(cl^6=?x 3B6 B:*5FxD#+ HPj chI]E쿴6o@42=D GLtܝ 4 +xkpp}3ƕ'l8  +a ]?P:Յ͌Q +>-3QrL;(ɍbP ]l\z ؓ;\VxiLO8${o\S#tRS_i@ /GJq[@txK>, T|Y*xagW&j +=|qfêB{  obb0OMhڤ'P|ya{g@GWm#Cfqu߬#>stream +8;X]O>EqN@%''O_@%e@?J;%+8(9e>X=MR6S?i^YgA3=].HDXF.R$lIL@"pJ+EP(%0 +b]6ajmNZn*!='OQZeQ^Y*,=]?C.B+\Ulg9dhD*"iC[;*=3`oP1[!S^)?1)IZ4dup` +E1r!/,*0[*9.aFIR2&b-C#soRZ7Dl%MLY\.?d>Mn +6%Q2oYfNRF$$+ON<+]RUJmC0InDZ4OTs0S!saG>GGKUlQ*Q?45:CI&4J'_2j$XKrcYp0n+Xl_nU*O( +l[$6Nn+Z_Nq0]s7hs]`XX$6Ra!<<'!!!*'!!rrmPX()~> endstream endobj 5 0 obj <> endobj 17 0 obj [/View/Design] endobj 18 0 obj <>>> endobj 9 0 obj <> endobj 6 0 obj [5 0 R] endobj 19 0 obj <> endobj xref +0 20 +0000000000 65535 f +0000000016 00000 n +0000000144 00000 n +0000022510 00000 n +0000000000 00000 f +0000071005 00000 n +0000071303 00000 n +0000022561 00000 n +0000022932 00000 n +0000071191 00000 n +0000023940 00000 n +0000024257 00000 n +0000070431 00000 n +0000024331 00000 n +0000024475 00000 n +0000026117 00000 n +0000070479 00000 n +0000071075 00000 n +0000071106 00000 n +0000071326 00000 n +trailer <]>> startxref 71548 %%EOF \ No newline at end of file diff --git a/assets/moex-most-logo/main/moex-most.png b/assets/moex-most-logo/main/moex-most.png new file mode 100755 index 0000000000000000000000000000000000000000..921628068eebc7153e58571aa6c0127bf13da672 GIT binary patch literal 13976 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/stable/manifest.json — подписанный SignedManifest +// /stable/bj-server — артефакты, перечисленные в манифесте +// /stable/crypto-service.jar +// /beta/manifest.json +// ... +// +// HTTP API (потребляет bj-server auto-update и install.sh): +// +// GET /v1//manifest.json — манифест канала +// GET /v1//files/ — артефакт по имени +// GET /healthz — проверка живости +// +// Подпись манифеста делает bj-release; здесь только статическая раздача. +// Перед прод-выкаткой ставится за TLS-reverse-proxy (nginx, см. +// deploy/artifactory/nginx.conf). +package main + +import ( + "flag" + "log" + "net/http" + "os" + "path/filepath" + "strings" + "time" +) + +func main() { + addr := flag.String("addr", ":8090", "адрес прослушивания") + root := flag.String("root", "./releases", "корень хранилища релизов") + flag.Parse() + + abs, err := filepath.Abs(*root) + if err != nil { + log.Fatalf("bj-artifactory: root: %v", err) + } + if _, err := os.Stat(abs); err != nil { + log.Fatalf("bj-artifactory: каталог релизов %s недоступен: %v", abs, err) + } + srv := &server{root: abs} + + mux := http.NewServeMux() + mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte("ok")) }) + mux.HandleFunc("/v1/", srv.handleV1) + + log.Printf("bj-artifactory: раздаю %s на %s", abs, *addr) + httpSrv := &http.Server{Addr: *addr, Handler: logging(mux), ReadHeaderTimeout: 10 * time.Second} + log.Fatal(httpSrv.ListenAndServe()) +} + +type server struct{ root string } + +// handleV1 разбирает /v1//manifest.json и /v1//files/. +func (s *server) handleV1(w http.ResponseWriter, r *http.Request) { + rest := strings.TrimPrefix(r.URL.Path, "/v1/") + parts := strings.SplitN(rest, "/", 3) + if len(parts) < 2 { + http.NotFound(w, r) + return + } + channel := parts[0] + if !safeName(channel) { + http.Error(w, "bad channel", http.StatusBadRequest) + return + } + switch { + case len(parts) == 2 && parts[1] == "manifest.json": + s.serveFile(w, r, filepath.Join(s.root, channel, "manifest.json"), "application/json") + case len(parts) == 3 && parts[1] == "files": + name := parts[2] + if !safeName(name) { + http.Error(w, "bad name", http.StatusBadRequest) + return + } + s.serveFile(w, r, filepath.Join(s.root, channel, name), "application/octet-stream") + default: + http.NotFound(w, r) + } +} + +func (s *server) serveFile(w http.ResponseWriter, r *http.Request, path, ctype string) { + f, err := os.Open(path) + if err != nil { + http.NotFound(w, r) + return + } + defer f.Close() + fi, err := f.Stat() + if err != nil || fi.IsDir() { + http.NotFound(w, r) + return + } + w.Header().Set("Content-Type", ctype) + http.ServeContent(w, r, filepath.Base(path), fi.ModTime(), f) +} + +// safeName запрещает обход каталогов (.., /, пустые). +func safeName(s string) bool { + if s == "" || s == "." || s == ".." { + return false + } + return !strings.ContainsAny(s, "/\\") && !strings.Contains(s, "..") +} + +func logging(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + next.ServeHTTP(w, r) + log.Printf("%s %s %s", r.RemoteAddr, r.Method, r.URL.Path) + }) +} diff --git a/cmd/bj-installer/main.go b/cmd/bj-installer/main.go new file mode 100644 index 0000000..a4af4a2 --- /dev/null +++ b/cmd/bj-installer/main.go @@ -0,0 +1,111 @@ +// Package main — bj-installer. +// +// Web-инсталлятор для bj-server: на машине клиента после установки +// Debian/Astra поднимает локальный HTTP на 127.0.0.1:8181, проводит +// через 5-страничный wizard (welcome → precheck → config → install → done) +// и за кадром выполняет 20+ шагов установки Валидаты + bj-server + ИШ. +// +// Прогресс шагов прилетает в UI через Server-Sent Events. Каждый шаг +// идемпотентен — можно повторно запускать инсталлятор на уже настроенной +// машине, он пропустит то, что сделано. +// +// Запуск: sudo ./bj-installer [--addr 127.0.0.1:8181] [--no-browser] +// Артефакты ожидаются рядом с бинарём в каталоге ./artifacts/: +// +// artifacts/ClientL_Other/zpki-*.deb +// artifacts/ClientL_Other/zsdk-*.deb +// artifacts/bj-server (Go-бинарь) +// artifacts/crypto-service.jar (Java-сайдкар) +// artifacts/ish/igate_*.deb (ИШ НРД) +package main + +import ( + "context" + "flag" + "fmt" + "log" + "net/http" + "os" + "os/exec" + "os/signal" + "runtime" + "syscall" + "time" +) + +const banner = ` +====================================================================== + bj-installer — мастер установки Bridge-and-Join-s +====================================================================== +` + +func main() { + addr := flag.String("addr", "127.0.0.1:8181", "адрес web-инсталлятора") + noBrowser := flag.Bool("no-browser", false, "не пытаться открыть браузер автоматически") + artifactsDir := flag.String("artifacts", "./artifacts", "каталог с дистрибутивами (Validata deb, bj-server, ish)") + flag.Parse() + + if os.Geteuid() != 0 { + fmt.Fprintln(os.Stderr, "Установщик должен быть запущен от root (sudo).") + os.Exit(1) + } + + fmt.Print(banner) + fmt.Printf(" адрес: http://%s\n", *addr) + fmt.Printf(" артефакты: %s\n", *artifactsDir) + fmt.Println("======================================================================") + + st := newState(*artifactsDir) + + srv := newServer(st) + httpSrv := &http.Server{ + Addr: *addr, + Handler: srv, + ReadHeaderTimeout: 10 * time.Second, + } + + // SIGINT/SIGTERM → корректный shutdown + ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) + defer stop() + + go func() { + if err := httpSrv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Fatalf("HTTP-сервер упал: %v", err) + } + }() + + url := "http://" + *addr + log.Printf("Откройте в браузере: %s", url) + if !*noBrowser { + tryOpenBrowser(url) + } + + <-ctx.Done() + log.Println("Завершаем работу...") + shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + _ = httpSrv.Shutdown(shutdownCtx) +} + +// tryOpenBrowser — без фанатизма. Если xdg-open/sensible-browser есть и +// $DISPLAY поднят (xrdp, Fly DE) — откроем. Иначе пользователь увидит URL +// в выводе и перейдёт сам с другого компа (типичный сценарий headless). +func tryOpenBrowser(url string) { + if os.Getenv("DISPLAY") == "" && os.Getenv("WAYLAND_DISPLAY") == "" { + return + } + var bin string + switch runtime.GOOS { + case "linux": + for _, cand := range []string{"xdg-open", "sensible-browser", "x-www-browser"} { + if p, err := exec.LookPath(cand); err == nil { + bin = p + break + } + } + } + if bin == "" { + return + } + _ = exec.Command(bin, url).Start() +} diff --git a/cmd/bj-installer/precheck.go b/cmd/bj-installer/precheck.go new file mode 100644 index 0000000..bacd592 --- /dev/null +++ b/cmd/bj-installer/precheck.go @@ -0,0 +1,132 @@ +package main + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "syscall" +) + +// runPrechecks — все системные проверки на стадии "Проверка системы". +// Возвращает срез результатов, по каждому видно ✓/✗ + объяснение. +// +// Ничего не модифицирует — просто читает /etc/os-release, проверяет +// наличие нужных бинарей, права root, свободное место, артефакты в +// artifactsDir и т.п. UI отрисовывает таблицей. +func runPrechecks(artifactsDir string) []PrecheckResult { + var out []PrecheckResult + + out = append(out, checkRoot()) + out = append(out, checkArch()) + out = append(out, checkDistro()) + out = append(out, checkAptAvailable()) + out = append(out, checkSystemd()) + out = append(out, checkDiskSpace()) + out = append(out, checkArtifacts(artifactsDir)) + + return out +} + +func checkRoot() PrecheckResult { + if os.Geteuid() == 0 { + return PrecheckResult{ID: "root", Title: "Запуск от root", OK: true} + } + return PrecheckResult{ID: "root", Title: "Запуск от root", OK: false, Message: "Требуется sudo"} +} + +func checkArch() PrecheckResult { + if runtime.GOARCH == "amd64" { + return PrecheckResult{ID: "arch", Title: "Архитектура amd64", OK: true, Message: runtime.GOARCH} + } + return PrecheckResult{ID: "arch", Title: "Архитектура amd64", OK: false, Message: "Валидата собрана только под amd64, у вас " + runtime.GOARCH} +} + +func checkDistro() PrecheckResult { + id, pretty := readOSRelease() + switch id { + case "debian", "astra": + return PrecheckResult{ID: "distro", Title: "Поддерживаемая ОС", OK: true, Message: pretty} + case "ubuntu": + return PrecheckResult{ID: "distro", Title: "Поддерживаемая ОС", OK: true, Message: pretty + " (поддерживается на свой страх)"} + default: + return PrecheckResult{ID: "distro", Title: "Поддерживаемая ОС", OK: false, Message: "ОС не в списке поддерживаемых: " + pretty} + } +} + +func checkAptAvailable() PrecheckResult { + if _, err := exec.LookPath("apt-get"); err != nil { + return PrecheckResult{ID: "apt", Title: "Доступен apt-get", OK: false, Message: "apt-get не найден — это не Debian-семейство"} + } + return PrecheckResult{ID: "apt", Title: "Доступен apt-get", OK: true} +} + +func checkSystemd() PrecheckResult { + if _, err := os.Stat("/run/systemd/system"); err != nil { + return PrecheckResult{ID: "systemd", Title: "systemd работает", OK: false, Message: "/run/systemd/system нет"} + } + return PrecheckResult{ID: "systemd", Title: "systemd работает", OK: true} +} + +func checkDiskSpace() PrecheckResult { + var fs syscall.Statfs_t + if err := syscall.Statfs("/var", &fs); err != nil { + return PrecheckResult{ID: "disk", Title: "Свободное место в /var", OK: false, Message: err.Error()} + } + freeBytes := fs.Bavail * uint64(fs.Bsize) + freeGiB := freeBytes / (1 << 30) + if freeGiB < 2 { + return PrecheckResult{ID: "disk", Title: "Свободное место в /var", OK: false, Message: fmt.Sprintf("Свободно %d GiB, нужно ≥ 2", freeGiB)} + } + return PrecheckResult{ID: "disk", Title: "Свободное место в /var", OK: true, Message: fmt.Sprintf("%d GiB свободно", freeGiB)} +} + +func checkArtifacts(dir string) PrecheckResult { + required := []struct { + Glob string + Name string + }{ + {filepath.Join(dir, "ClientL_Other", "zpki-*.deb"), "zpki (Валидата)"}, + {filepath.Join(dir, "bj-server"), "bj-server (Go-бинарь)"}, + {filepath.Join(dir, "crypto-service.jar"), "crypto-service.jar"}, + } + var missing []string + for _, r := range required { + matches, _ := filepath.Glob(r.Glob) + if len(matches) == 0 { + missing = append(missing, r.Name) + } + } + if len(missing) > 0 { + return PrecheckResult{ + ID: "artifacts", + Title: "Артефакты дистрибутива", + OK: false, + Message: "Отсутствуют: " + strings.Join(missing, ", ") + " (положите в " + dir + ")", + } + } + return PrecheckResult{ID: "artifacts", Title: "Артефакты дистрибутива", OK: true, Message: "Все на месте в " + dir} +} + +func readOSRelease() (id, pretty string) { + b, err := os.ReadFile("/etc/os-release") + if err != nil { + return "", "неизвестно" + } + for _, line := range strings.Split(string(b), "\n") { + k, v, ok := strings.Cut(line, "=") + if !ok { + continue + } + v = strings.Trim(v, `"`) + switch k { + case "ID": + id = v + case "PRETTY_NAME": + pretty = v + } + } + return +} diff --git a/cmd/bj-installer/server.go b/cmd/bj-installer/server.go new file mode 100644 index 0000000..463cca8 --- /dev/null +++ b/cmd/bj-installer/server.go @@ -0,0 +1,129 @@ +package main + +import ( + "embed" + "encoding/json" + "io/fs" + "log" + "net/http" + "strings" +) + +//go:embed web +var webFS embed.FS + +type server struct { + state *State + mux *http.ServeMux +} + +func newServer(st *State) *server { + s := &server{state: st, mux: http.NewServeMux()} + + // Статика (HTML/CSS/JS из embed) + sub, _ := fs.Sub(webFS, "web") + s.mux.Handle("/", http.FileServer(http.FS(sub))) + + // API + s.mux.HandleFunc("/api/state", s.handleState) + s.mux.HandleFunc("/api/precheck", s.handlePrecheck) + s.mux.HandleFunc("/api/config", s.handleConfig) + s.mux.HandleFunc("/api/install", s.handleInstall) + s.mux.HandleFunc("/api/events", s.handleSSE) + s.mux.HandleFunc("/api/reset", s.handleReset) + + return s +} + +func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // Защита: только localhost (даже если addr 0.0.0.0 поставят) + host := r.RemoteAddr + if i := strings.LastIndex(host, ":"); i != -1 { + host = host[:i] + } + switch host { + case "127.0.0.1", "::1", "[::1]", "localhost": + // ok + default: + http.Error(w, "installer is local-only", http.StatusForbidden) + return + } + s.mux.ServeHTTP(w, r) +} + +// GET /api/state — полный snapshot для холодного открытия страницы. +func (s *server) handleState(w http.ResponseWriter, r *http.Request) { + snap := s.state.Snapshot() + writeJSON(w, snap) +} + +// POST /api/precheck — запускает все pre-check проверки и возвращает результат. +// Wizard переходит на стадию precheck. +func (s *server) handlePrecheck(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "POST only", http.StatusMethodNotAllowed) + return + } + s.state.setStage(StagePrecheck) + results := runPrechecks(s.state.artifactsDir) + s.state.setPrecheck(results) + writeJSON(w, results) +} + +// POST /api/config — сохраняет org INN, email, license. Переход на стадию config. +func (s *server) handleConfig(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "POST only", http.StatusMethodNotAllowed) + return + } + var c Config + if err := json.NewDecoder(r.Body).Decode(&c); err != nil { + http.Error(w, "invalid json", http.StatusBadRequest) + return + } + s.state.setConfig(c) + s.state.setStage(StageConfig) + writeJSON(w, map[string]bool{"ok": true}) +} + +// POST /api/install — стартует установку (в горутине), переход на стадию installing. +// UI слушает /api/events для прогресса. +func (s *server) handleInstall(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "POST only", http.StatusMethodNotAllowed) + return + } + s.state.setStage(StageInstalling) + go func() { + if err := runInstallation(s.state); err != nil { + log.Printf("install error: %v", err) + s.state.setError(err.Error()) + return + } + s.state.setStage(StageDone) + }() + writeJSON(w, map[string]bool{"ok": true}) +} + +// POST /api/reset — сброс wizard'а на welcome (после ошибки). +func (s *server) handleReset(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "POST only", http.StatusMethodNotAllowed) + return + } + s.state.mu.Lock() + s.state.Stage = StageWelcome + s.state.ErrorMsg = "" + s.state.Precheck = nil + s.state.Steps = buildStepList() + s.state.mu.Unlock() + s.state.bus.publish(event{Type: "reset", Data: "{}"}) + writeJSON(w, map[string]bool{"ok": true}) +} + +func writeJSON(w http.ResponseWriter, v any) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + enc := json.NewEncoder(w) + enc.SetIndent("", " ") + _ = enc.Encode(v) +} diff --git a/cmd/bj-installer/sse.go b/cmd/bj-installer/sse.go new file mode 100644 index 0000000..8bf7df5 --- /dev/null +++ b/cmd/bj-installer/sse.go @@ -0,0 +1,86 @@ +package main + +import ( + "fmt" + "net/http" + "sync" +) + +// event — одно событие, отдаваемое подписчикам через SSE. +// Type становится `event:` строкой, Data — `data:`. +type event struct { + Type string + Data string +} + +// eventBus — простой fan-out для SSE. Подписчик создаётся в момент +// открытия GET /api/events и живёт до закрытия соединения. +type eventBus struct { + mu sync.Mutex + subscribers map[chan event]struct{} +} + +func newEventBus() *eventBus { + return &eventBus{subscribers: make(map[chan event]struct{})} +} + +func (b *eventBus) subscribe() chan event { + ch := make(chan event, 64) + b.mu.Lock() + b.subscribers[ch] = struct{}{} + b.mu.Unlock() + return ch +} + +func (b *eventBus) unsubscribe(ch chan event) { + b.mu.Lock() + delete(b.subscribers, ch) + close(ch) + b.mu.Unlock() +} + +func (b *eventBus) publish(e event) { + b.mu.Lock() + defer b.mu.Unlock() + for ch := range b.subscribers { + select { + case ch <- e: + default: + // Подписчик отстаёт — пропускаем (UI догонится снапшотом по GET /api/state) + } + } +} + +// handleSSE — GET /api/events. Держит соединение, в каждом событии +// отдаёт event: \ndata: \n\n. +func (s *server) handleSSE(w http.ResponseWriter, r *http.Request) { + flusher, ok := w.(http.Flusher) + if !ok { + http.Error(w, "streaming not supported", http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "text/event-stream") + w.Header().Set("Cache-Control", "no-cache") + w.Header().Set("Connection", "keep-alive") + w.WriteHeader(http.StatusOK) + + ch := s.state.bus.subscribe() + defer s.state.bus.unsubscribe(ch) + + // сразу шлём snapshot, чтобы UI догнал состояние + snap := s.state.Snapshot() + fmt.Fprintf(w, "event: snapshot\ndata: %s\n\n", mustJSON(snap)) + flusher.Flush() + + for { + select { + case <-r.Context().Done(): + return + case e := <-ch: + if _, err := fmt.Fprintf(w, "event: %s\ndata: %s\n\n", e.Type, e.Data); err != nil { + return + } + flusher.Flush() + } + } +} diff --git a/cmd/bj-installer/state.go b/cmd/bj-installer/state.go new file mode 100644 index 0000000..8d7cc1a --- /dev/null +++ b/cmd/bj-installer/state.go @@ -0,0 +1,140 @@ +package main + +import ( + "encoding/json" + "sync" + "time" +) + +// WizardStage — какой странице wizard'а соответствует текущее состояние. +// Переходы: welcome → precheck → config → installing → done. +// Из любого можно вернуться в welcome (полный reset). +type WizardStage string + +const ( + StageWelcome WizardStage = "welcome" + StagePrecheck WizardStage = "precheck" + StageConfig WizardStage = "config" + StageInstalling WizardStage = "installing" + StageDone WizardStage = "done" + StageError WizardStage = "error" +) + +// Config — данные, которые wizard собирает на стадии config. +type Config struct { + OrgINN string `json:"orgInn"` // ИНН организации + OrgName string `json:"orgName"` // отображаемое имя + AdminEmail string `json:"adminEmail"` // куда писать алерты + LicenseKey string `json:"licenseKey"` // годовой ключ (опционально, можно пропустить) +} + +// StepStatus — текущее состояние конкретного шага установки. +type StepStatus string + +const ( + StepPending StepStatus = "pending" + StepRunning StepStatus = "running" + StepDone StepStatus = "done" + StepSkipped StepStatus = "skipped" + StepFailed StepStatus = "failed" +) + +// StepState — снимок одного шага для отдачи в UI. +type StepState struct { + ID string `json:"id"` + Title string `json:"title"` + Status StepStatus `json:"status"` + Message string `json:"message,omitempty"` + Started *time.Time `json:"started,omitempty"` + Finished *time.Time `json:"finished,omitempty"` +} + +// PrecheckResult — результат одной системной проверки на стадии precheck. +type PrecheckResult struct { + ID string `json:"id"` + Title string `json:"title"` + OK bool `json:"ok"` + Message string `json:"message,omitempty"` +} + +// State — потокобезопасное состояние wizard'а. Хранит всё что нужно +// отрисовать на любой из страниц + текущий прогресс установки. +type State struct { + mu sync.RWMutex + + artifactsDir string + + Stage WizardStage `json:"stage"` + ErrorMsg string `json:"errorMsg,omitempty"` + Precheck []PrecheckResult `json:"precheck"` + Config Config `json:"config"` + Steps []StepState `json:"steps"` + + bus *eventBus +} + +func newState(artifactsDir string) *State { + return &State{ + artifactsDir: artifactsDir, + Stage: StageWelcome, + Steps: buildStepList(), + bus: newEventBus(), + } +} + +// Snapshot — потокобезопасная копия для GET /api/state. +func (s *State) Snapshot() State { + s.mu.RLock() + defer s.mu.RUnlock() + cp := *s + cp.Precheck = append([]PrecheckResult(nil), s.Precheck...) + cp.Steps = append([]StepState(nil), s.Steps...) + return cp +} + +func (s *State) setStage(st WizardStage) { + s.mu.Lock() + s.Stage = st + s.mu.Unlock() + s.bus.publish(event{Type: "stage", Data: mustJSON(map[string]string{"stage": string(st)})}) +} + +func (s *State) setError(msg string) { + s.mu.Lock() + s.Stage = StageError + s.ErrorMsg = msg + s.mu.Unlock() + s.bus.publish(event{Type: "error", Data: mustJSON(map[string]string{"message": msg})}) +} + +func (s *State) setPrecheck(items []PrecheckResult) { + s.mu.Lock() + s.Precheck = items + s.mu.Unlock() + s.bus.publish(event{Type: "precheck", Data: mustJSON(items)}) +} + +func (s *State) setConfig(c Config) { + s.mu.Lock() + s.Config = c + s.mu.Unlock() +} + +func (s *State) updateStep(id string, fn func(*StepState)) { + s.mu.Lock() + var snap StepState + for i := range s.Steps { + if s.Steps[i].ID == id { + fn(&s.Steps[i]) + snap = s.Steps[i] + break + } + } + s.mu.Unlock() + s.bus.publish(event{Type: "step", Data: mustJSON(snap)}) +} + +func mustJSON(v any) string { + b, _ := json.Marshal(v) + return string(b) +} diff --git a/cmd/bj-installer/steps.go b/cmd/bj-installer/steps.go new file mode 100644 index 0000000..dd0a6ec --- /dev/null +++ b/cmd/bj-installer/steps.go @@ -0,0 +1,84 @@ +package main + +import ( + "fmt" + "time" +) + +// Step — описание одного шага установки. Run выполняет шаг, может +// проверить idempotency и вернуть Skipped. Логи прокидываются через +// log-функцию, которая публикует event в SSE. +type Step struct { + ID string + Title string + Run func(s *State, log func(string)) (StepStatus, error) +} + +// buildStepList — фиксированный порядок шагов установки. Соответствует +// install-validata.sh + установка bj-server/crypto-service/ИШ. Меняется +// атомарно (если что-то добавляется — добавляем сюда). +func buildStepList() []StepState { + steps := allSteps() + out := make([]StepState, len(steps)) + for i, s := range steps { + out[i] = StepState{ID: s.ID, Title: s.Title, Status: StepPending} + } + return out +} + +func allSteps() []Step { + return []Step{ + {ID: "deps", Title: "Установка системных зависимостей", Run: stepInstallDeps}, + {ID: "validata-deb", Title: "Установка пакетов Валидаты (zpki + zsdk)", Run: stepInstallValidataDebs}, + {ID: "execstack", Title: "execstack -c libvdcsp.so", Run: stepExecstack}, + {ID: "bj-user", Title: "Создание пользователя bj и каталогов", Run: stepCreateBJUser}, + {ID: "pcscd-dropin", Title: "Настройка pcscd (always-on)", Run: stepPcscdDropin}, + {ID: "bj-crypto-dropins", Title: "Drop-ins для bj-crypto sandbox", Run: stepBJCryptoDropins}, + {ID: "bj-server-dropin", Title: "Drop-in для bj-server", Run: stepBJServerDropin}, + {ID: "spki-ini", Title: "Создание spki.ini", Run: stepSPKIIni}, + {ID: "pki1-prep", Title: "Подготовка pki1.conf для bj", Run: stepPKI1Prep}, + {ID: "usb-mount", Title: "Авто-mount USB через udev + systemd", Run: stepUSBMount}, + {ID: "bj-server-binary", Title: "Установка bj-server бинаря в /opt/bj/", Run: stepInstallBJServer}, + {ID: "crypto-jar", Title: "Установка crypto-service.jar", Run: stepInstallCryptoJar}, + {ID: "systemd-units", Title: "systemd unit bj-crypto.service + bj-server.service", Run: stepSystemdUnits}, + {ID: "ish-install", Title: "Установка ИШ НРД (если есть .deb)", Run: stepInstallISH}, + {ID: "save-config", Title: "Сохранение setup.json", Run: stepSaveConfig}, + {ID: "systemd-start", Title: "Запуск сервисов (pcscd, bj-crypto, bj-server)", Run: stepStartServices}, + {ID: "health", Title: "Финальный health-check", Run: stepHealthCheck}, + } +} + +// runInstallation — основной цикл установки. Перебирает шаги, обновляет +// статусы через State, прокидывает логи в SSE. Останавливается при первой +// ошибке (UI покажет какой шаг + сообщение). +func runInstallation(s *State) error { + steps := allSteps() + for _, step := range steps { + now := time.Now() + s.updateStep(step.ID, func(ss *StepState) { + ss.Status = StepRunning + ss.Started = &now + ss.Message = "" + }) + + logFn := func(line string) { + s.updateStep(step.ID, func(ss *StepState) { + ss.Message = line + }) + } + + status, err := step.Run(s, logFn) + finished := time.Now() + s.updateStep(step.ID, func(ss *StepState) { + ss.Status = status + ss.Finished = &finished + if err != nil { + ss.Message = err.Error() + } + }) + if err != nil { + return fmt.Errorf("шаг %q: %w", step.Title, err) + } + } + return nil +} diff --git a/cmd/bj-installer/steps_impl.go b/cmd/bj-installer/steps_impl.go new file mode 100644 index 0000000..486ef79 --- /dev/null +++ b/cmd/bj-installer/steps_impl.go @@ -0,0 +1,448 @@ +package main + +import ( + "encoding/json" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" +) + +// --------------------------------------------------------------------- // +// Хелперы +// --------------------------------------------------------------------- // + +// runCmd — запускает команду, прокидывает stdout/stderr построчно в log. +// Возвращает ошибку с последними строками stderr для удобства отображения. +func runCmd(logFn func(string), name string, args ...string) error { + logFn(fmt.Sprintf("$ %s %s", name, strings.Join(args, " "))) + cmd := exec.Command(name, args...) + out, err := cmd.CombinedOutput() + for _, line := range strings.Split(strings.TrimRight(string(out), "\n"), "\n") { + if line != "" { + logFn(line) + } + } + if err != nil { + return fmt.Errorf("%s: %w", name, err) + } + return nil +} + +// writeFileIfChanged — пишет файл только если содержимое отличается. Возвращает +// true если файл был создан/изменён (для решения «нужен ли daemon-reload»). +func writeFileIfChanged(path string, content string, mode os.FileMode) (bool, error) { + existing, err := os.ReadFile(path) + if err == nil && string(existing) == content { + return false, nil + } + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + return false, err + } + if err := os.WriteFile(path, []byte(content), mode); err != nil { + return false, err + } + return true, nil +} + +// --------------------------------------------------------------------- // +// Шаги +// --------------------------------------------------------------------- // + +func stepInstallDeps(s *State, log func(string)) (StepStatus, error) { + log("Обновляю apt-кеш...") + if err := runCmd(log, "apt-get", "update", "-qq"); err != nil { + return StepFailed, err + } + + deps := []string{ + "libgtk-3-0", "libpcsclite1", "libccid", "pcscd", + "libcurl4", "libkrb5-3", "libgssapi-krb5-2", + "libsasl2-modules", "libsasl2-modules-gssapi-mit", + "execstack", "p7zip-full", + } + if hasAPTPackage("libldap-2.4-2") { + deps = append(deps, "libldap-2.4-2") + } else { + deps = append(deps, "libldap-2.5-0") + log("libldap-2.4-2 не найден → ставлю 2.5-0, для zpki будет --force-depends") + } + + args := append([]string{"install", "-y", "--no-install-recommends"}, deps...) + if err := runCmd(log, "apt-get", args...); err != nil { + return StepFailed, err + } + return StepDone, nil +} + +func stepInstallValidataDebs(s *State, log func(string)) (StepStatus, error) { + zpki, _ := filepath.Glob(filepath.Join(s.artifactsDir, "ClientL_Other", "zpki-*.amd64.deb")) + zsdk, _ := filepath.Glob(filepath.Join(s.artifactsDir, "ClientL_Other", "zsdk-*.amd64.deb")) + if len(zpki) == 0 { + return StepFailed, fmt.Errorf("zpki-*.amd64.deb не найден в %s/ClientL_Other/", s.artifactsDir) + } + useForce := !hasAPTPackage("libldap-2.4-2") + for _, deb := range append(zpki, zsdk...) { + args := []string{"-i", deb} + if useForce { + args = append([]string{"--force-depends"}, args...) + } + if err := runCmd(log, "dpkg", args...); err != nil { + return StepFailed, err + } + } + if _, err := os.Stat("/opt/Validata/VDCSP/lib/amd64"); err != nil { + return StepFailed, fmt.Errorf("/opt/Validata не появился после установки") + } + return StepDone, nil +} + +func stepExecstack(s *State, log func(string)) (StepStatus, error) { + target := "/opt/Validata/VDCSP/lib/amd64/libvdcsp.so" + // Проверка состояния + out, err := exec.Command("execstack", "-q", target).Output() + if err == nil && strings.HasPrefix(strings.TrimSpace(string(out)), "-") { + log("executable-stack уже снят") + return StepSkipped, nil + } + return StepDone, runCmd(log, "execstack", "-c", target) +} + +func stepCreateBJUser(s *State, log func(string)) (StepStatus, error) { + if _, err := exec.LookPath("id"); err == nil { + if exec.Command("id", "bj").Run() == nil { + log("Пользователь bj уже существует") + } else { + if err := runCmd(log, "useradd", "--system", "--create-home", + "--home-dir", "/var/lib/bj", "--shell", "/bin/bash", "bj"); err != nil { + return StepFailed, err + } + } + } + dirs := []struct { + Path string + Mode os.FileMode + }{ + {"/var/lib/bj/usb", 0o755}, + {"/var/lib/bj/.Validata", 0o700}, + {"/var/lib/bj/.Validata/vdkeys", 0o700}, + {"/var/lib/bj/profiles", 0o755}, + {"/var/log/bj", 0o755}, + {"/var/lib/bj/.bj", 0o700}, + } + for _, d := range dirs { + if err := os.MkdirAll(d.Path, d.Mode); err != nil { + return StepFailed, err + } + } + return StepDone, runCmd(log, "chown", "-R", "bj:bj", "/var/lib/bj", "/var/log/bj") +} + +func stepPcscdDropin(s *State, log func(string)) (StepStatus, error) { + const dropin = `[Unit] +Requires= +After= +Sockets= + +[Service] +ExecStart= +ExecStart=/usr/sbin/pcscd --foreground +` + changed, err := writeFileIfChanged("/etc/systemd/system/pcscd.service.d/no-autoexit.conf", dropin, 0o644) + if err != nil { + return StepFailed, err + } + if !changed { + log("Drop-in уже актуален") + return StepSkipped, nil + } + log("Создан /etc/systemd/system/pcscd.service.d/no-autoexit.conf") + return StepDone, nil +} + +func stepBJCryptoDropins(s *State, log func(string)) (StepStatus, error) { + files := map[string]string{ + "/etc/systemd/system/bj-crypto.service.d/validata-paths.conf": `[Service] +WorkingDirectory=/opt/Validata/VDCSP/etc +ReadWritePaths=/opt/Validata/VDCSP/etc +ReadWritePaths=/var/lib/bj +`, + "/etc/systemd/system/bj-crypto.service.d/usb-access.conf": `[Service] +ReadOnlyPaths=/media +ReadOnlyPaths=/var/lib/bj/usb +`, + "/etc/systemd/system/bj-crypto.service.d/share-crysvc.conf": `[Service] +PrivateTmp=true +BindPaths=/tmp/.crysvc.sock:/tmp/.crysvc.sock +`, + } + for path, content := range files { + if _, err := writeFileIfChanged(path, content, 0o644); err != nil { + return StepFailed, err + } + } + return StepDone, nil +} + +func stepBJServerDropin(s *State, log func(string)) (StepStatus, error) { + const dropin = `[Service] +ReadWritePaths=/opt/Validata/VDCSP/etc +` + _, err := writeFileIfChanged("/etc/systemd/system/bj-server.service.d/pki1conf.conf", dropin, 0o644) + if err != nil { + return StepFailed, err + } + return StepDone, nil +} + +func stepSPKIIni(s *State, log func(string)) (StepStatus, error) { + const path = "/opt/Validata/VDCSP/etc/spki.ini" + if _, err := os.Stat(path); err == nil { + log("Файл уже существует") + return StepSkipped, nil + } + const content = `[store] +count = 0 + +[Parameters] +PkiLdapTimeout = 10 +PkiHttpTimeout = 60 +` + if err := os.WriteFile(path, []byte(content), 0o644); err != nil { + return StepFailed, err + } + return StepDone, nil +} + +func stepPKI1Prep(s *State, log func(string)) (StepStatus, error) { + const path = "/opt/Validata/VDCSP/etc/pki1.conf" + if _, err := os.Stat(path); err != nil { + log("Файл pki1.conf отсутствует — Валидата создаст при первом запуске") + return StepSkipped, nil + } + if err := runCmd(log, "chgrp", "bj", path); err != nil { + return StepFailed, err + } + if err := runCmd(log, "chmod", "g+w", path); err != nil { + return StepFailed, err + } + + existing, _ := os.ReadFile(path) + if !strings.Contains(string(existing), "# --- bj-server: BEGIN ---") { + appended := string(existing) + "\n# --- bj-server: BEGIN ---\n# Секции профилей дописываются bj-server при импорте через /admin/setup.\n# --- bj-server: END ---\n" + if err := os.WriteFile(path, []byte(appended), 0o664); err != nil { + return StepFailed, err + } + } + return StepDone, nil +} + +func stepUSBMount(s *State, log func(string)) (StepStatus, error) { + files := map[string]string{ + "/etc/udev/rules.d/99-bj-usb.rules": `# Авто-mount USB-флешек в /var/lib/bj/usb/ с владельцем bj. +ACTION=="add", KERNEL=="sd?[0-9]", SUBSYSTEMS=="usb", \ + ENV{ID_FS_TYPE}!="", \ + ENV{SYSTEMD_WANTS}="bj-usb-mount@$env{ID_FS_UUID}.service" +ACTION=="remove", KERNEL=="sd?[0-9]", SUBSYSTEMS=="usb", \ + ENV{ID_FS_TYPE}!="", \ + ENV{SYSTEMD_WANTS}="bj-usb-umount@$env{ID_FS_UUID}.service" +`, + "/etc/systemd/system/bj-usb-mount@.service": `[Unit] +Description=Mount USB %i to /var/lib/bj/usb/%i for bj +DefaultDependencies=no +After=local-fs.target + +[Service] +Type=oneshot +RemainAfterExit=yes +ExecStart=/usr/bin/bash -c 'mkdir -p /var/lib/bj/usb/%i && /usr/bin/mount -o uid=$(id -u bj),gid=$(id -g bj),fmask=0133,dmask=0022 UUID=%i /var/lib/bj/usb/%i' +ExecStop=/usr/bin/umount /var/lib/bj/usb/%i || true +`, + "/etc/systemd/system/bj-usb-umount@.service": `[Unit] +Description=Umount USB %i from /var/lib/bj/usb/%i +DefaultDependencies=no + +[Service] +Type=oneshot +ExecStart=/usr/bin/bash -c '/usr/bin/umount /var/lib/bj/usb/%i 2>/dev/null; /usr/bin/rmdir /var/lib/bj/usb/%i 2>/dev/null; true' +`, + } + anyChanged := false + for path, content := range files { + ch, err := writeFileIfChanged(path, content, 0o644) + if err != nil { + return StepFailed, err + } + anyChanged = anyChanged || ch + } + if anyChanged { + _ = runCmd(log, "udevadm", "control", "--reload-rules") + _ = runCmd(log, "udevadm", "trigger") + } + return StepDone, nil +} + +func stepInstallBJServer(s *State, log func(string)) (StepStatus, error) { + src := filepath.Join(s.artifactsDir, "bj-server") + if _, err := os.Stat(src); err != nil { + return StepSkipped, nil // нет бинаря — может ставится через rpm/deb + } + if err := os.MkdirAll("/opt/bj", 0o755); err != nil { + return StepFailed, err + } + if err := runCmd(log, "install", "-o", "bj", "-g", "bj", "-m", "0755", src, "/opt/bj/bj-server"); err != nil { + return StepFailed, err + } + return StepDone, nil +} + +func stepInstallCryptoJar(s *State, log func(string)) (StepStatus, error) { + src := filepath.Join(s.artifactsDir, "crypto-service.jar") + if _, err := os.Stat(src); err != nil { + return StepSkipped, nil + } + if err := os.MkdirAll("/opt/bj", 0o755); err != nil { + return StepFailed, err + } + if err := runCmd(log, "install", "-o", "bj", "-g", "bj", "-m", "0644", src, "/opt/bj/crypto-service.jar"); err != nil { + return StepFailed, err + } + return StepDone, nil +} + +func stepSystemdUnits(s *State, log func(string)) (StepStatus, error) { + units := map[string]string{ + "/etc/systemd/system/bj-crypto.service": `[Unit] +Description=Bridge-and-Join-s — Crypto sidecar (Java + Валидата Клиент L) +Before=bj-server.service +After=network-online.target pcscd.service +Wants=network-online.target + +[Service] +Type=simple +User=bj +Group=bj +RuntimeDirectory=bj +RuntimeDirectoryMode=0750 +Environment=BJ_CRYPTO_SOCKET=/run/bj/crypto.sock +Environment=BJ_CRYPTO_PROVIDER=validata +Environment=LD_LIBRARY_PATH=/opt/Validata/VDCSP/lib/amd64 +ExecStart=/usr/bin/java -Djava.library.path=/opt/Validata/VDCSP/lib/amd64 -jar /opt/bj/crypto-service.jar +Restart=on-failure +RestartSec=5 +StandardOutput=append:/var/log/bj/crypto-service.log +StandardError=append:/var/log/bj/crypto-service.err +NoNewPrivileges=true +ProtectSystem=strict +ProtectHome=true +ReadWritePaths=/run/bj /var/log/bj +PrivateTmp=true + +[Install] +WantedBy=multi-user.target +`, + "/etc/systemd/system/bj-server.service": `[Unit] +Description=Bridge-and-Join-s — единый сервис M2M-переводов +After=network-online.target bj-crypto.service +Wants=network-online.target + +[Service] +Type=simple +User=bj +Group=bj +WorkingDirectory=/var/lib/bj +ExecStart=/opt/bj/bj-server +Restart=on-failure +RestartSec=5 +Environment=BJ_HTTP_ADDR=:8080 +Environment=BJ_SETUP_PATH=/var/lib/bj/.bj/setup.json +ProtectSystem=strict +ProtectHome=true +ReadWritePaths=/var/lib/bj /var/log/bj + +[Install] +WantedBy=multi-user.target +`, + } + for path, content := range units { + if _, err := writeFileIfChanged(path, content, 0o644); err != nil { + return StepFailed, err + } + } + return StepDone, runCmd(log, "systemctl", "daemon-reload") +} + +func stepInstallISH(s *State, log func(string)) (StepStatus, error) { + matches, _ := filepath.Glob(filepath.Join(s.artifactsDir, "ish", "igate_*.deb")) + if len(matches) == 0 { + log("Дистрибутив ИШ не найден — пропускаю (можно установить позже)") + return StepSkipped, nil + } + if err := runCmd(log, "dpkg", "-i", matches[0]); err != nil { + // допустим, что зависимости подтянутся + _ = runCmd(log, "apt-get", "-f", "install", "-y") + if err := runCmd(log, "dpkg", "-i", matches[0]); err != nil { + return StepFailed, err + } + } + return StepDone, nil +} + +func stepSaveConfig(s *State, log func(string)) (StepStatus, error) { + cfg := s.Snapshot().Config + if cfg.OrgINN == "" && cfg.AdminEmail == "" && cfg.LicenseKey == "" { + return StepSkipped, nil + } + b, err := json.MarshalIndent(cfg, "", " ") + if err != nil { + return StepFailed, err + } + if err := os.MkdirAll("/var/lib/bj/.bj", 0o700); err != nil { + return StepFailed, err + } + if err := os.WriteFile("/var/lib/bj/.bj/setup.json", b, 0o600); err != nil { + return StepFailed, err + } + return StepDone, runCmd(log, "chown", "-R", "bj:bj", "/var/lib/bj/.bj") +} + +func stepStartServices(s *State, log func(string)) (StepStatus, error) { + // disable+stop socket activation для pcscd + _ = runCmd(log, "systemctl", "stop", "pcscd.socket") + _ = runCmd(log, "systemctl", "disable", "pcscd.socket") + + for _, svc := range []string{"pcscd", "bj-crypto", "bj-server"} { + if err := runCmd(log, "systemctl", "enable", svc); err != nil { + return StepFailed, err + } + if err := runCmd(log, "systemctl", "restart", svc); err != nil { + return StepFailed, err + } + } + return StepDone, nil +} + +func stepHealthCheck(s *State, log func(string)) (StepStatus, error) { + var bad []string + for _, svc := range []string{"pcscd", "vdcrysvc", "bj-crypto", "bj-server"} { + if err := exec.Command("systemctl", "is-active", "--quiet", svc).Run(); err != nil { + bad = append(bad, svc) + } else { + log(svc + ": active") + } + } + if len(bad) > 0 { + return StepFailed, fmt.Errorf("сервисы не запустились: %s", strings.Join(bad, ", ")) + } + return StepDone, nil +} + +// hasAPTPackage — проверяет наличие пакета в apt-cache (доступен ли для установки). +func hasAPTPackage(name string) bool { + out, err := exec.Command("apt-cache", "show", name).CombinedOutput() + if err != nil { + return false + } + return strings.Contains(string(out), "Package: "+name) +} diff --git a/cmd/bj-installer/web/app.js b/cmd/bj-installer/web/app.js new file mode 100644 index 0000000..43222cd --- /dev/null +++ b/cmd/bj-installer/web/app.js @@ -0,0 +1,168 @@ +// Минимальный клиент wizard'а: рендерит страницы, ловит события из SSE, +// отправляет POST'ы на backend для перехода между стадиями. + +let state = { + stage: "welcome", + precheck: [], + config: {}, + steps: [], + errorMsg: "", +}; + +const STAGE_ORDER = ["welcome", "precheck", "config", "installing", "done"]; +const STEP_ICONS = { + pending: "○", + running: "◐", + done: "✓", + skipped: "—", + failed: "✗", +}; + +function $(sel) { return document.querySelector(sel); } +function $$(sel) { return [...document.querySelectorAll(sel)]; } + +function render() { + // stepper + $$("#stepper span").forEach(el => { + el.classList.remove("active", "done"); + const stage = el.dataset.stage; + if (stage === state.stage) el.classList.add("active"); + if (STAGE_ORDER.indexOf(stage) < STAGE_ORDER.indexOf(state.stage)) el.classList.add("done"); + }); + // pages + $$(".page").forEach(p => p.classList.toggle("active", p.dataset.stage === state.stage)); + + if (state.stage === "precheck") renderPrecheck(); + if (state.stage === "installing" || state.stage === "done") renderSteps(); + if (state.stage === "error") $("#error-message").textContent = state.errorMsg || "(нет деталей)"; + if (state.stage === "done") { + // подставляем хост машины в админскую ссылку + const adminURL = window.location.protocol + "//" + window.location.hostname + ":8080/admin/setup"; + $("#adminLink").href = adminURL; + $("#adminLink").textContent = "Перейти в " + adminURL + " →"; + } +} + +function renderPrecheck() { + const root = $("#precheck-results"); + root.innerHTML = ""; + let allOK = true; + for (const r of state.precheck || []) { + const div = document.createElement("div"); + div.className = "check " + (r.ok ? "ok" : "bad"); + div.innerHTML = ` + ${r.ok ? "✓" : "✗"} +
+
${escapeHTML(r.title)}
+ ${r.message ? `
${escapeHTML(r.message)}
` : ""} +
`; + root.appendChild(div); + if (!r.ok) allOK = false; + } + $("#goConfigBtn").disabled = !allOK; +} + +function renderSteps() { + const root = $("#step-list"); + root.innerHTML = ""; + let done = 0; + for (const s of state.steps || []) { + const li = document.createElement("li"); + li.className = "step-" + s.status; + li.innerHTML = ` + ${STEP_ICONS[s.status] || "○"} +
+
${escapeHTML(s.title)}
+ ${s.message ? `
${escapeHTML(s.message)}
` : ""} +
`; + root.appendChild(li); + if (s.status === "done" || s.status === "skipped") done++; + } + const total = state.steps.length; + const pct = total ? Math.round(100 * done / total) : 0; + $("#progress-bar").style.width = pct + "%"; +} + +function escapeHTML(s) { + return String(s).replace(/[&<>"']/g, c => ({ + "&": "&", "<": "<", ">": ">", "\"": """, "'": "'" + }[c])); +} + +// ------------- transitions ------------- + +async function startPrecheck() { + await fetch("/api/precheck", { method: "POST" }); +} + +function goWelcome() { + state.stage = "welcome"; + render(); +} + +function goPrecheck() { + state.stage = "precheck"; + render(); +} + +async function goConfig() { + state.stage = "config"; + render(); +} + +async function startInstall() { + const form = $("#config-form"); + const data = Object.fromEntries(new FormData(form).entries()); + await fetch("/api/config", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(data), + }); + await fetch("/api/install", { method: "POST" }); +} + +async function resetWizard() { + await fetch("/api/reset", { method: "POST" }); +} + +// ------------- SSE ------------- + +function connectSSE() { + const es = new EventSource("/api/events"); + es.addEventListener("snapshot", e => { + const snap = JSON.parse(e.data); + state.stage = snap.stage; + state.precheck = snap.precheck || []; + state.config = snap.config || {}; + state.steps = snap.steps || []; + state.errorMsg = snap.errorMsg || ""; + render(); + }); + es.addEventListener("stage", e => { + state.stage = JSON.parse(e.data).stage; + render(); + }); + es.addEventListener("precheck", e => { + state.precheck = JSON.parse(e.data); + render(); + }); + es.addEventListener("step", e => { + const s = JSON.parse(e.data); + const idx = state.steps.findIndex(x => x.id === s.id); + if (idx >= 0) state.steps[idx] = s; + render(); + }); + es.addEventListener("error", e => { + state.errorMsg = JSON.parse(e.data).message; + state.stage = "error"; + render(); + }); + es.addEventListener("reset", () => { + location.reload(); + }); + es.onerror = () => { + // авто-реконнект делает EventSource сам, ничего не делаем + }; +} + +connectSSE(); diff --git a/cmd/bj-installer/web/index.html b/cmd/bj-installer/web/index.html new file mode 100644 index 0000000..aaab131 --- /dev/null +++ b/cmd/bj-installer/web/index.html @@ -0,0 +1,110 @@ + + + + + +bj-installer — мастер установки Bridge-and-Join-s + + + +
+ +
мастер установки
+
+ +
+ + + + +
+

Добро пожаловать

+

Этот мастер установит на сервер СКЗИ «Валидата Клиент L», + bj-server, bj-crypto и ИШ НРД, настроит + systemd-сервисы и подготовит окружение для подписи документов + по ГОСТ 34.10-2012.

+

После установки откроется /admin/setup в bj-server, где можно + загрузить тестовый профиль от MOEX (.7z) и активировать подпись.

+
+ +
+
+ + +
+

Проверка системы

+
+
+ + +
+
+ + +
+

Настройка

+
+ + + + +
+ + +
+
+
+ + +
+

Установка

+
    +
    +
    + + +
    +

    ✓ Готово

    +

    bj-server и все сервисы запущены. Откройте панель администратора и + импортируйте профиль:

    + +

    Что дальше:

    +
      +
    1. Подключите USB с .vdk → он автоматически смонтируется в /var/lib/bj/usb/
    2. +
    3. На /admin/setup загрузите .7z с профилем от MOEX и введите пароль
    4. +
    5. Нажмите «Активировать» — bj-crypto подтянет ключ и подтвердит готовность
    6. +
    +
    + + +
    +

    ✗ Установка прервана

    +

    Произошла ошибка:

    +
    
    +    

    Логи: journalctl -u bj-installer и journalctl -u bj-crypto

    +
    + +
    +
    +
    + + + + diff --git a/cmd/bj-installer/web/style.css b/cmd/bj-installer/web/style.css new file mode 100644 index 0000000..6730da9 --- /dev/null +++ b/cmd/bj-installer/web/style.css @@ -0,0 +1,179 @@ +:root { + --bg: #f6f7fb; + --card: #ffffff; + --text: #1d2330; + --muted: #6b7280; + --accent: #2563eb; + --accent-dark: #1d4ed8; + --ok: #16a34a; + --err: #dc2626; + --border: #e5e7eb; +} +* { box-sizing: border-box; } +html, body { + margin: 0; padding: 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + background: var(--bg); + color: var(--text); + line-height: 1.5; +} +.topbar { + display: flex; align-items: baseline; gap: 16px; + padding: 18px 32px; + background: #0f172a; + color: #fff; + border-bottom: 1px solid #1e293b; +} +.logo { font-weight: 700; font-size: 18px; letter-spacing: 0.3px; } +.subtitle { color: #94a3b8; font-size: 14px; } +main { + max-width: 760px; + margin: 24px auto; + padding: 0 16px; +} +.stepper { + display: flex; gap: 8px; + margin-bottom: 24px; + padding: 12px 14px; + background: var(--card); + border: 1px solid var(--border); + border-radius: 10px; + font-size: 13px; + overflow-x: auto; +} +.stepper span { + padding: 6px 12px; + border-radius: 6px; + color: var(--muted); + white-space: nowrap; +} +.stepper span.active { + background: var(--accent); + color: #fff; + font-weight: 600; +} +.stepper span.done { + color: var(--ok); +} +.page { + display: none; + background: var(--card); + border: 1px solid var(--border); + border-radius: 12px; + padding: 28px 32px; +} +.page.active { display: block; } +h1 { margin: 0 0 16px; font-size: 22px; } +p, label { font-size: 15px; } +.muted { color: var(--muted); font-size: 13px; } +code { + background: #f1f5f9; + padding: 2px 6px; + border-radius: 4px; + font-size: 13px; +} +.buttons { + display: flex; gap: 12px; + margin-top: 24px; + justify-content: flex-end; +} +button, .primary-link { + padding: 10px 18px; + font-size: 14px; + font-weight: 500; + border: 1px solid var(--border); + background: #fff; + color: var(--text); + border-radius: 8px; + cursor: pointer; + text-decoration: none; + display: inline-block; +} +button.primary, .primary-link { + background: var(--accent); + color: #fff; + border-color: var(--accent); +} +button.primary:hover, .primary-link:hover { background: var(--accent-dark); } +button:disabled { opacity: 0.5; cursor: not-allowed; } + +/* Precheck */ +.checks { display: flex; flex-direction: column; gap: 10px; margin-top: 12px; } +.check { + display: flex; gap: 12px; align-items: center; + padding: 12px 14px; + border: 1px solid var(--border); + border-radius: 8px; +} +.check.ok { border-color: var(--ok); } +.check.bad { border-color: var(--err); } +.check-icon { font-size: 18px; } +.check.ok .check-icon { color: var(--ok); } +.check.bad .check-icon { color: var(--err); } +.check-title { font-weight: 500; } +.check-msg { font-size: 13px; color: var(--muted); } + +/* Config form */ +#config-form { display: flex; flex-direction: column; gap: 16px; } +#config-form label { + display: flex; flex-direction: column; gap: 4px; + font-weight: 500; +} +#config-form input { + padding: 10px 12px; + border: 1px solid var(--border); + border-radius: 8px; + font: inherit; + background: #fff; +} +#config-form input:focus { outline: 2px solid var(--accent); outline-offset: -1px; border-color: var(--accent); } + +/* Installing steps */ +.steps { list-style: none; padding: 0; margin: 16px 0; } +.steps li { + display: flex; gap: 12px; align-items: flex-start; + padding: 10px 0; + border-bottom: 1px solid var(--border); +} +.steps li:last-child { border-bottom: none; } +.step-icon { font-size: 16px; min-width: 24px; line-height: 1.5; } +.step-title { font-weight: 500; } +.step-msg { font-size: 12px; color: var(--muted); margin-top: 2px; word-break: break-word; } +.step-pending .step-icon { color: var(--muted); } +.step-running .step-icon { color: var(--accent); animation: spin 1.2s linear infinite; } +.step-done .step-icon { color: var(--ok); } +.step-skipped .step-icon { color: var(--muted); } +.step-failed .step-icon { color: var(--err); } +@keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} +.progress { + margin-top: 16px; + background: var(--border); + border-radius: 4px; + height: 6px; + overflow: hidden; +} +.progress-bar { + height: 100%; + width: 0%; + background: var(--accent); + transition: width 0.3s ease; +} + +/* Done */ +.next-link { margin: 20px 0; } + +/* Error */ +.error { + background: #fef2f2; + border: 1px solid var(--err); + color: var(--err); + padding: 12px; + border-radius: 8px; + font-family: ui-monospace, "SF Mono", Menlo, monospace; + font-size: 13px; + overflow-x: auto; + white-space: pre-wrap; +} diff --git a/cmd/bj-license-server/main.go b/cmd/bj-license-server/main.go new file mode 100644 index 0000000..fb96b9b --- /dev/null +++ b/cmd/bj-license-server/main.go @@ -0,0 +1,96 @@ +// Command bj-license-server — онлайн-сервис учёта и отзыва лицензий. +// +// Базовая модель лицензирования офлайновая: bj-server проверяет подпись и +// срок сам. Этот сервер нужен для: +// - реестра выданных лицензий (учёт); +// - ОТЗЫВА (revocation) до окончания срока; +// - проверки клиентом «не отозвана ли» (опциональный online-чек). +// +// Хранилище — JSON-файл со списком отозванных ID (для каркаса; в проде — +// PostgreSQL). API: +// +// GET /v1/check?id= → {"revoked":bool} +// GET /healthz +// +// Управление отзывом — правкой файла revoked.json (или будущим admin API). +package main + +import ( + "encoding/json" + "flag" + "log" + "net/http" + "os" + "sync" + "time" +) + +type store struct { + mu sync.RWMutex + path string + revoked map[string]bool +} + +func newStore(path string) *store { + s := &store{path: path, revoked: map[string]bool{}} + s.load() + return s +} + +func (s *store) load() { + s.mu.Lock() + defer s.mu.Unlock() + b, err := os.ReadFile(s.path) + if err != nil { + return // файла нет — пустой список + } + var ids []string + if err := json.Unmarshal(b, &ids); err != nil { + log.Printf("license-server: разбор %s: %v", s.path, err) + return + } + s.revoked = map[string]bool{} + for _, id := range ids { + s.revoked[id] = true + } + log.Printf("license-server: загружено отозванных лицензий: %d", len(s.revoked)) +} + +func (s *store) isRevoked(id string) bool { + s.mu.RLock() + defer s.mu.RUnlock() + return s.revoked[id] +} + +func main() { + addr := flag.String("addr", ":8091", "адрес прослушивания") + file := flag.String("revoked", "./revoked.json", "JSON-файл со списком отозванных license ID") + flag.Parse() + + st := newStore(*file) + + // Перечитываем файл отзывов раз в минуту (горячее применение). + go func() { + t := time.NewTicker(time.Minute) + defer t.Stop() + for range t.C { + st.load() + } + }() + + mux := http.NewServeMux() + mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte("ok")) }) + mux.HandleFunc("/v1/check", func(w http.ResponseWriter, r *http.Request) { + id := r.URL.Query().Get("id") + if id == "" { + http.Error(w, "id required", http.StatusBadRequest) + return + } + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]bool{"revoked": st.isRevoked(id)}) + }) + + log.Printf("license-server: слушаю %s, отзывы из %s", *addr, *file) + srv := &http.Server{Addr: *addr, Handler: mux, ReadHeaderTimeout: 10 * time.Second} + log.Fatal(srv.ListenAndServe()) +} diff --git a/cmd/bj-license/main.go b/cmd/bj-license/main.go new file mode 100644 index 0000000..dc09886 --- /dev/null +++ b/cmd/bj-license/main.go @@ -0,0 +1,192 @@ +// Command bj-license — инструмент издателя: генерация ключей подписи, +// выпуск годовых лицензий и проверка. +// +// bj-license keygen -out ./keys/license +// bj-license issue -tenant "ООО Ромашка" -plan pro -days 365 \ +// -features updates,web-cabinet -key ./keys/license.priv -keyid main +// bj-license verify -key-file license.key -pub ./keys/license.pub +package main + +import ( + "crypto/ed25519" + "crypto/rand" + "encoding/base64" + "encoding/hex" + "fmt" + "os" + "strings" + "time" + + "git.zetit.ru/zuevav/Bridge-and-Join-s/internal/license" +) + +// newUUID — UUID v4 без внешних зависимостей. +func newUUID() string { + var b [16]byte + _, _ = rand.Read(b[:]) + b[6] = (b[6] & 0x0f) | 0x40 // версия 4 + b[8] = (b[8] & 0x3f) | 0x80 // вариант + h := hex.EncodeToString(b[:]) + return h[0:8] + "-" + h[8:12] + "-" + h[12:16] + "-" + h[16:20] + "-" + h[20:32] +} + +func main() { + if len(os.Args) < 2 { + usage() + } + switch os.Args[1] { + case "keygen": + keygen(os.Args[2:]) + case "issue": + issue(os.Args[2:]) + case "verify": + verify(os.Args[2:]) + default: + usage() + } +} + +func usage() { + fmt.Fprintln(os.Stderr, "bj-license keygen -out ") + fmt.Fprintln(os.Stderr, "bj-license issue -tenant -plan free|pro|enterprise -days -features a,b -key [-keyid id] [-max-nodes n] [-note txt]") + fmt.Fprintln(os.Stderr, "bj-license verify -key-file -pub ") + os.Exit(2) +} + +func keygen(args []string) { + out := "license" + for i := 0; i < len(args)-1; i++ { + if args[i] == "-out" { + out = args[i+1] + } + } + pub, priv, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + fatal("keygen: %v", err) + } + if err := os.WriteFile(out+".priv", []byte(base64.StdEncoding.EncodeToString(priv.Seed())+"\n"), 0o600); err != nil { + fatal("write priv: %v", err) + } + if err := os.WriteFile(out+".pub", []byte(base64.StdEncoding.EncodeToString(pub)+"\n"), 0o644); err != nil { + fatal("write pub: %v", err) + } + fmt.Printf("Приватный ключ лицензий: %s.priv (СЕКРЕТ)\n", out) + fmt.Printf("Публичный ключ (зашить в bj-server):\n %s\n", base64.StdEncoding.EncodeToString(pub)) +} + +func issue(args []string) { + a := parseArgs(args) + tenant := a["tenant"] + keyPath := a["key"] + if tenant == "" || keyPath == "" { + fatal("issue: требуются -tenant и -key") + } + plan := license.Plan(orDefault(a["plan"], "pro")) + days := atoiDefault(a["days"], 365) + keyID := orDefault(a["keyid"], "main") + + priv, err := license.LoadPrivateKey(keyPath) + if err != nil { + fatal("load key: %v", err) + } + + now := time.Now().UTC() + var feats []string + if a["features"] != "" { + feats = strings.Split(a["features"], ",") + } + l := &license.License{ + Schema: license.CurrentSchema, + ID: newUUID(), + Tenant: tenant, + Product: "bj-server", + Plan: plan, + IssuedAt: now, + ExpiresAt: now.AddDate(0, 0, days), + Features: feats, + MaxNodes: atoiDefault(a["max-nodes"], 0), + Note: a["note"], + } + tok, err := license.Sign(l, priv, keyID) + if err != nil { + fatal("sign: %v", err) + } + fmt.Printf("Лицензия выпущена: tenant=%q plan=%s до %s (%d дней)\n", + tenant, plan, l.ExpiresAt.Format("02.01.2006"), days) + fmt.Printf("ID: %s\n", l.ID) + fmt.Println("Ключ для клиента (вставить в bj-server → Лицензия):") + fmt.Println(tok.Encode()) +} + +func verify(args []string) { + a := parseArgs(args) + if a["key-file"] == "" || a["pub"] == "" { + fatal("verify: требуются -key-file и -pub") + } + raw, err := os.ReadFile(a["key-file"]) + if err != nil { + fatal("read key-file: %v", err) + } + pubB, err := os.ReadFile(a["pub"]) + if err != nil { + fatal("read pub: %v", err) + } + pub, err := license.ParsePublicKey(strings.TrimSpace(string(pubB))) + if err != nil { + fatal("pub: %v", err) + } + tok, err := license.DecodeToken(string(raw)) + if err != nil { + fatal("decode: %v", err) + } + l, err := license.Verify(tok, pub) + if err != nil { + fatal("verify: %v", err) + } + now := time.Now().UTC() + fmt.Printf("Подпись валидна. tenant=%q plan=%s\n", l.Tenant, l.Plan) + fmt.Printf("Действует: %s — %s (осталось %d дней)\n", + l.IssuedAt.Format("02.01.2006"), l.ExpiresAt.Format("02.01.2006"), l.DaysLeft(now)) + if err := l.Valid(now); err != nil { + fmt.Printf("СТАТУС: %v\n", err) + } else { + fmt.Printf("СТАТУС: активна, обновления %v\n", l.AllowsUpdates()) + } +} + +// --- helpers --- + +func parseArgs(args []string) map[string]string { + m := map[string]string{} + for i := 0; i < len(args); i++ { + if strings.HasPrefix(args[i], "-") && i+1 < len(args) { + m[strings.TrimPrefix(args[i], "-")] = args[i+1] + i++ + } + } + return m +} + +func orDefault(s, def string) string { + if s == "" { + return def + } + return s +} + +func atoiDefault(s string, def int) int { + if s == "" { + return def + } + var n int + _, err := fmt.Sscanf(s, "%d", &n) + if err != nil { + return def + } + return n +} + +func fatal(format string, a ...any) { + fmt.Fprintf(os.Stderr, "bj-license: "+format+"\n", a...) + os.Exit(1) +} diff --git a/cmd/bj-release/main.go b/cmd/bj-release/main.go new file mode 100644 index 0000000..65f5d36 --- /dev/null +++ b/cmd/bj-release/main.go @@ -0,0 +1,169 @@ +// Command bj-release — инструмент издателя: генерация ключей подписи, +// сборка манифеста релиза из каталога артефактов и его подпись Ed25519. +// +// Использование: +// +// bj-release keygen -out ./keys/signing +// → создаёт signing.priv (base64 seed) и signing.pub (base64 pubkey) +// +// bj-release build -dir ./dist -version 1.2.0 -channel stable \ +// -key ./keys/signing.priv -keyid main -out ./dist/manifest.json +// → хеширует все файлы в ./dist, собирает Manifest, подписывает, +// пишет SignedManifest в manifest.json +// +// Манифест подписывается целиком; клиент (bj-server auto-update) проверяет +// подпись зашитым публичным ключом ДО доверия версиям/хешам. +package main + +import ( + "crypto/ed25519" + "crypto/rand" + "encoding/base64" + "encoding/json" + "flag" + "fmt" + "os" + "path/filepath" + "sort" + "strings" + "time" + + "git.zetit.ru/zuevav/Bridge-and-Join-s/internal/release" +) + +func main() { + if len(os.Args) < 2 { + usage() + } + switch os.Args[1] { + case "keygen": + keygen(os.Args[2:]) + case "build": + build(os.Args[2:]) + default: + usage() + } +} + +func usage() { + fmt.Fprintln(os.Stderr, "bj-release keygen -out ") + fmt.Fprintln(os.Stderr, "bj-release build -dir -version -channel -key -keyid -out [-notes ]") + os.Exit(2) +} + +func keygen(args []string) { + fs := flag.NewFlagSet("keygen", flag.ExitOnError) + out := fs.String("out", "signing", "префикс файлов ключей (создаст .priv и .pub)") + _ = fs.Parse(args) + + pub, priv, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + fatal("keygen: %v", err) + } + seed := priv.Seed() + if err := os.WriteFile(*out+".priv", []byte(base64.StdEncoding.EncodeToString(seed)+"\n"), 0o600); err != nil { + fatal("write priv: %v", err) + } + if err := os.WriteFile(*out+".pub", []byte(base64.StdEncoding.EncodeToString(pub)+"\n"), 0o644); err != nil { + fatal("write pub: %v", err) + } + fmt.Printf("Приватный ключ: %s.priv (НЕ КОММИТИТЬ, держать в секрете)\n", *out) + fmt.Printf("Публичный ключ: %s.pub\n", *out) + fmt.Printf("Публичный ключ (зашить в bj-server):\n %s\n", base64.StdEncoding.EncodeToString(pub)) +} + +func build(args []string) { + fs := flag.NewFlagSet("build", flag.ExitOnError) + dir := fs.String("dir", "./dist", "каталог с артефактами") + version := fs.String("version", "", "версия релиза, напр. 1.2.0") + channel := fs.String("channel", "stable", "канал: stable|beta") + keyPath := fs.String("key", "", "путь к приватному ключу (base64 seed)") + keyID := fs.String("keyid", "main", "идентификатор ключа") + out := fs.String("out", "", "путь для записи manifest.json (по умолчанию /manifest.json)") + notes := fs.String("notes", "", "заметки к релизу") + _ = fs.Parse(args) + + if *version == "" || *keyPath == "" { + fatal("build: требуются -version и -key") + } + if *out == "" { + *out = filepath.Join(*dir, "manifest.json") + } + + priv, err := release.LoadPrivateKey(*keyPath) + if err != nil { + fatal("load key: %v", err) + } + + // Имена артефактов, которые издаём (логическое имя → ставить +x). + known := map[string]bool{ + "bj-server": true, // Go-бинарь + "crypto-service.jar": false, // Java сайдкар + "install-validata.sh": true, + "install.sh": true, + "configure-ish.sql": false, + } + + entries, err := os.ReadDir(*dir) + if err != nil { + fatal("read dir: %v", err) + } + var arts []release.Artifact + for _, e := range entries { + if e.IsDir() || e.Name() == "manifest.json" { + continue + } + full := filepath.Join(*dir, e.Name()) + sha, size, err := release.HashFile(full) + if err != nil { + fatal("hash %s: %v", e.Name(), err) + } + exec, ok := known[e.Name()] + if !ok { + // неизвестный файл — включаем, +x по расширению + exec = strings.HasSuffix(e.Name(), ".sh") + } + arts = append(arts, release.Artifact{ + Name: e.Name(), + File: e.Name(), + Version: *version, + SHA256: sha, + Size: size, + Exec: exec, + }) + } + sort.Slice(arts, func(i, j int) bool { return arts[i].Name < arts[j].Name }) + if len(arts) == 0 { + fatal("build: в каталоге %s нет артефактов", *dir) + } + + m := &release.Manifest{ + Schema: release.CurrentSchema, + Version: *version, + Channel: *channel, + ReleasedAt: time.Now().UTC(), + Notes: *notes, + Artifacts: arts, + } + sm, err := release.Sign(m, priv, *keyID) + if err != nil { + fatal("sign: %v", err) + } + b, err := json.MarshalIndent(sm, "", " ") + if err != nil { + fatal("marshal: %v", err) + } + if err := os.WriteFile(*out, b, 0o644); err != nil { + fatal("write manifest: %v", err) + } + fmt.Printf("Манифест %s: версия %s, канал %s, артефактов %d, подписан ключом %s\n", + *out, *version, *channel, len(arts), *keyID) + for _, a := range arts { + fmt.Printf(" %-22s %10d B %s\n", a.Name, a.Size, a.SHA256[:16]) + } +} + +func fatal(format string, a ...any) { + fmt.Fprintf(os.Stderr, "bj-release: "+format+"\n", a...) + os.Exit(1) +} diff --git a/cmd/bj-server/main.go b/cmd/bj-server/main.go index 3ee9e08..cfa3ffe 100644 --- a/cmd/bj-server/main.go +++ b/cmd/bj-server/main.go @@ -40,17 +40,10 @@ func main() { DefaultSender: defaultSender, DefaultReceiver: defaultReceiver, SetupPath: setupPath, - CheckOptions: func() lkgateway.CheckOptions { - return lkgateway.CheckOptions{ - PostgresDSN: os.Getenv("BJ_DSN"), - CryptoSocket: getenv("BJ_CRYPTO_SOCKET", "/run/bj/crypto.sock"), - NSDAdapterURL: os.Getenv("BJ_NSD_ADAPTER_URL"), - LKCallbackURL: os.Getenv("BJ_LK_CALLBACK_URL"), - Profile: getenv("BJ_NSD_PROFILE", "demo (mock NSD)"), - CryptoProvider: getenv("BJ_CRYPTO_PROVIDER", "stub"), - Timeout: 2 * time.Second, - } - }, + // CheckOptions не задаём — server.go использует свой снапшот-based + // вариант, который читает актуальные значения из setup.json + // (DSN, crypto-сокет, URL ИШ, профиль), а не из ENV. Так проверки + // статуса совпадают с тем, что реально настроено в UI. } srv, err := lkgateway.NewServer(cfg) diff --git a/deploy/artifactory/README.md b/deploy/artifactory/README.md new file mode 100644 index 0000000..0432342 --- /dev/null +++ b/deploy/artifactory/README.md @@ -0,0 +1,79 @@ +# Артефактория Bridge-and-Join-s + +Сервис раздачи релизов и обновлений (#18). Клиенты (bj-server auto-update, +install.sh) скачивают **подписанный** манифест канала, проверяют подпись +зашитым публичным ключом и обновляют компоненты. + +## Компоненты + +- `internal/release` — формат манифеста + подпись Ed25519 (sign/verify, хеши). +- `cmd/bj-release` — инструмент издателя: генерация ключей, сборка и подпись манифеста. +- `cmd/bj-artifactory` — HTTP-сервер раздачи манифеста и артефактов. +- `deploy/artifactory/` — nginx (TLS) + systemd unit. + +## Модель доверия + +Один корневой Ed25519-ключ. Приватный (`signing.priv`) держит издатель в +секрете (НЕ в git). Публичный (`signing.pub`) зашивается в bj-server и в +install.sh. Манифест подписывается целиком — клиент проверяет подпись ДО +доверия версиям и хешам артефактов, затем сверяет sha256 каждого скачанного +файла с манифестом. + +## Релизный цикл (издатель) + +```bash +# 1. Однократно — сгенерировать ключи подписи (приватный хранить в секрете!) +bj-release keygen -out ./keys/signing +# → keys/signing.priv (секрет), keys/signing.pub +# Публичный base64 из вывода — зашить в bj-server (auto-update, #20) + +# 2. Собрать артефакты релиза в каталог +mkdir -p dist/stable +cp bj-server crypto-service.jar dist/stable/ +cp deploy/linux/install-validata.sh deploy/ish/configure-ish.sql dist/stable/ + +# 3. Собрать + подписать манифест +bj-release build -dir dist/stable -version 1.0.0 -channel stable \ + -key keys/signing.priv -keyid main -out dist/stable/manifest.json \ + -notes "Первый релиз" + +# 4. Выложить каталог в хранилище артефактории +rsync -a dist/stable/ server:/var/lib/bj-artifactory/releases/stable/ +``` + +## Сервер + +```bash +bj-artifactory --addr 127.0.0.1:8090 --root /var/lib/bj-artifactory/releases +``` + +Раскладка хранилища: + +``` +releases/ + stable/ + manifest.json ← подписанный SignedManifest + bj-server + crypto-service.jar + install-validata.sh + configure-ish.sql + beta/ + manifest.json + ... +``` + +## HTTP API + +| Метод | Путь | Ответ | +|---|---|---| +| GET | `/v1//manifest.json` | подписанный манифест канала | +| GET | `/v1//files/` | артефакт по имени | +| GET | `/healthz` | `ok` | + +За TLS-reverse-proxy (`nginx.conf`). Прод: `updates.example.com` → 127.0.0.1:8090. + +## Дальше + +- **#19 License-сервер** — манифест/обновления гейтятся годовым ключом. +- **#20 Auto-update в bj-server** — горутина: качает манифест канала, проверяет + подпись, сравнивает версии, atomic-replace бинарей, systemctl restart. diff --git a/deploy/artifactory/artifactory.service b/deploy/artifactory/artifactory.service new file mode 100644 index 0000000..dd93b42 --- /dev/null +++ b/deploy/artifactory/artifactory.service @@ -0,0 +1,21 @@ +[Unit] +Description=Bridge-and-Join-s — Artifactory (раздача релизов и обновлений) +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +User=bj-updates +Group=bj-updates +ExecStart=/opt/bj-artifactory/bj-artifactory --addr 127.0.0.1:8090 --root /var/lib/bj-artifactory/releases +Restart=on-failure +RestartSec=5 + +NoNewPrivileges=true +ProtectSystem=strict +ProtectHome=true +ReadOnlyPaths=/var/lib/bj-artifactory +PrivateTmp=true + +[Install] +WantedBy=multi-user.target diff --git a/deploy/artifactory/nginx.conf b/deploy/artifactory/nginx.conf new file mode 100644 index 0000000..5c26341 --- /dev/null +++ b/deploy/artifactory/nginx.conf @@ -0,0 +1,44 @@ +# nginx.conf — reverse-proxy для bj-artifactory с TLS. +# Раздаёт релизы и обновления bj-server по HTTPS. +# +# Установка: положить в /etc/nginx/sites-available/, заменить server_name +# и пути сертификатов, выпустить TLS через certbot, symlink в sites-enabled. +# +# updates.example.com → bj-artifactory на 127.0.0.1:8090 +# +# bj-artifactory запускается как systemd-сервис (см. artifactory.service). + +server { + listen 80; + server_name updates.example.com; + # Редирект на HTTPS (кроме ACME-челленджа certbot). + location /.well-known/acme-challenge/ { root /var/www/certbot; } + location / { return 301 https://$host$request_uri; } +} + +server { + listen 443 ssl http2; + server_name updates.example.com; + + ssl_certificate /etc/letsencrypt/live/updates.example.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/updates.example.com/privkey.pem; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_prefer_server_ciphers on; + + # Манифесты маленькие — не кэшируем агрессивно (быстрое распространение релизов). + location /v1/ { + proxy_pass http://127.0.0.1:8090; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + # Артефакты могут быть крупными (jar ~20МБ) — без буферизации тела. + proxy_buffering off; + client_max_body_size 0; + } + + location /healthz { + proxy_pass http://127.0.0.1:8090; + } + + # Всё остальное — 404. + location / { return 404; } +} diff --git a/deploy/ish/channel-reference.txt b/deploy/ish/channel-reference.txt new file mode 100644 index 0000000..35b4dd5 --- /dev/null +++ b/deploy/ish/channel-reference.txt @@ -0,0 +1 @@ +TEST3 GOST|TEST3GOST|WSL|t diff --git a/deploy/ish/configure-ish.sql b/deploy/ish/configure-ish.sql new file mode 100644 index 0000000..76d8cd1 --- /dev/null +++ b/deploy/ish/configure-ish.sql @@ -0,0 +1,132 @@ +-- configure-ish.sql — автонастройка ИШ НРД без GUI. +-- +-- Снято как эталон с рабочей GUI-конфигурации (deploy/ish/params-reference.txt) +-- и параметризовано. Воспроизводит то, что оператор делал бы мышкой в +-- igate.exe (Avalonia): PostgreSQL + Web API + WSL-канал TEST3-GOST. +-- +-- Применяется к свежей БД ИШ ПОСЛЕ того как схема создана через +-- `igate-cli --data ` (он накатывает EF-миграции при первом подключении). +-- +-- Подстановки (заменяются установщиком через psql -v): +-- :channel_name — отображаемое имя канала, напр. 'TEST3 GOST' +-- :channel_code — локальный код канала, напр. 'TEST3GOST' +-- :wsl_endpoint — URL службы WSL НРД (TEST3-GOST) +-- :crypto_profile — имя профиля Валидаты ('moex') +-- :repository_code— код депонента из письма НРД ('MC0079200000') +-- :exchange_dir — рабочая папка обмена ('/var/lib/igate/exchange') +-- :web_port — порт Web API ('8090') +-- +-- Пример: +-- psql -h 127.0.0.1 -U igate -d igate \ +-- -v channel_name="'TEST3 GOST'" -v channel_code="'TEST3GOST'" \ +-- -v wsl_endpoint="'https://gost-t3.nsd.ru/onyx-ms/OnyxEdoWSService/OnyxEdo'" \ +-- -v crypto_profile="'moex'" -v repository_code="'MC0079200000'" \ +-- -v exchange_dir="'/var/lib/igate/exchange'" -v web_port="'8090'" \ +-- -f configure-ish.sql + +BEGIN; + +-- Чистим прежнюю конфигурацию (идемпотентность) +DELETE FROM parameters; +DELETE FROM channels; + +-- --- Глобальные параметры: Web API (КРИТИЧНО — runEngineOnStartApp=True, +-- иначе движок не стартует в headless-режиме и Kestrel не поднимается) --- +INSERT INTO parameters(name, value, chanel_id) VALUES + ('runEngineOnStartApp', 'True', NULL), + ('server.useServer', 'True', NULL), + ('server.host', 'localhost', NULL), + ('server.port', :web_port, NULL), + ('server.scheme', 'Http', NULL), + ('server.authentication.enable', 'False', NULL), + ('server.authentication.userName', '', NULL), + ('server.authentication.password', '', NULL), + ('server.certificate.storage', 'File', NULL), + ('server.certificate.store.location', 'CurrentUser', NULL), + ('server.certificate.store.name', 'My', NULL), + ('server.certificate.file.path', '', NULL), + ('server.certificate.file.password', '', NULL), + ('wsl.httpsMode', 'Auto', NULL), + ('wsl.maxConnsPerServer', '4', NULL), + ('wsl.proxy.mode', 'None', NULL), + ('wsl.proxy.address', '', NULL), + ('wsl.proxy.port', '0', NULL), + ('wsl.proxy.username', '', NULL), + ('wsl.proxy.password', '', NULL), + ('enableDbLogging', 'False', NULL), + ('cleanAutomatically', 'False', NULL), + ('cleanAtTime', '00:30:00', NULL), + ('cleanWhenLarger', '1024', NULL), + ('cleanVacuum', 'False', NULL), + ('storePeriod', '15', NULL), + ('archiveAutomatically', 'False', NULL); + +-- --- WSL-канал --- +-- ВАЖНО: ИШ резолвит канал по СОСТАВНОМУ коду = <код канала> + <код депонента> +-- (так формирует ИШ-GUI: TEST3 + MC0413600000 = TEST3MC0413600000). С коротким +-- кодом ИШ падает 'more than one Channel' и admin API не видит канал. +INSERT INTO channels(name, code, type, enable) +VALUES (:channel_name, :channel_code || :repository_code, 'WSL', true); + +-- Параметры канала привязываем к его id (находим по составному коду) +INSERT INTO parameters(name, value, chanel_id) +SELECT n, v, c.id FROM channels c, (VALUES + ('enable', 'True'), + ('wslEndpoint', :wsl_endpoint), + ('cryptography.type', 'GOST'), + ('cryptography.profile', :crypto_profile), + ('cryptography.pincode', ''), + ('cryptography.clientCertificateSerialNumber', ''), + ('repositoryCode', :repository_code), + ('fetchInterval', '00:01:00'), + ('attemptInterval', '30000'), + ('sendAttempts', '3'), + ('maxPartSize', '500'), + ('loadOldMessagesDepth', '3'), + ('isIncomingEnabled', 'True'), + ('isOutgoingEnabled', 'True'), + ('isTransitTerminalChannel', 'False'), + ('useDirectories', 'True'), + ('dir', :exchange_dir), + ('inboxDirName', 'INBOX'), + ('outboxDirName', 'OUTBOX'), + ('sentDirName', 'SENT'), + ('errorDirName', 'ERRORS'), + ('archive1042sDirName', :exchange_dir || '/Archives1042S'), + ('enableLockFile', 'True'), + ('enableAutoResponse', 'True'), + ('enable1042ReportProcessing', 'True'), + ('RenameOutgoingFiles', 'True'), + ('generateReceivedPackageInfo', 'True'), + ('generateSentPackageInfo', 'False'), + ('moveReceiptsToSentFolder', 'False'), + ('applyAddHashOfPackageToFolder', 'False'), + ('ignorePackageDirectoryStructure', 'False'), + ('checkReceivedPackageNsdSign', 'False'), + ('checkReceivedPackageSenderSign', 'False'), + ('autoUpdateTransitMember', 'False'), + ('automaticcalyLoadCrls', 'False'), + ('autoInPkgReportOffload', 'False'), + ('monitoringThreshold', '00:00:10'), + -- Пустые параметры для полного соответствия эталону GUI (движок ожидает + -- их наличие; отсутствие части может дать «Invalid value» при старте). + ('autoLoadCrlsTime', ''), + ('fetchThreadCount', ''), + ('forceCryPackageEncryption', ''), + ('inPkgReportDirectory', ''), + ('inPkgReportOffloadInterval', ''), + ('maxPackagesPerJob', ''), + ('nsdCertificateSerialNumbers', ''), + ('pkiDecryptMode', ''), + ('pkiEncryptMode', ''), + ('pkiSignMode', ''), + ('pkiVerifyMode', ''), + ('receiveProcThreadCount', ''), + ('sendProcThreadCount', ''), + ('updateTransitMemberListTime', '') +) AS p(n, v) +WHERE c.code = :channel_code || :repository_code; + +COMMIT; + +\echo 'ИШ настроен. Перезапустите igate-svc: systemctl restart igate' diff --git a/deploy/ish/params-reference.txt b/deploy/ish/params-reference.txt new file mode 100644 index 0000000..b12c47d --- /dev/null +++ b/deploy/ish/params-reference.txt @@ -0,0 +1,83 @@ +archiveAtTime||NULL +archiveAutomatically|False|NULL +archiveRecordsOlderThan||NULL +archiveWhenLarger||NULL +cleanAtTime|00:30:00|NULL +cleanAutomatically|False|NULL +cleanVacuum|False|NULL +cleanWhenLarger|1024|NULL +enableDbLogging|False|NULL +httpTimeout||NULL +packageBackupFolder||NULL +runEngineOnStartApp|False|NULL +server.authentication.enable|False|NULL +server.authentication.password||NULL +server.authentication.userName||NULL +server.certificate.file.password||NULL +server.certificate.file.path||NULL +server.certificate.storage|File|NULL +server.certificate.store.location|CurrentUser|NULL +server.certificate.store.name|My|NULL +server.host|localhost|NULL +server.port|8090|NULL +server.scheme|Http|NULL +server.useServer|True|NULL +storePeriod|15|NULL +wsl.httpsMode|Auto|NULL +wsl.maxConnsPerServer|4|NULL +wsl.proxy.address||NULL +wsl.proxy.mode|None|NULL +wsl.proxy.password||NULL +wsl.proxy.port|0|NULL +wsl.proxy.username||NULL +applyAddHashOfPackageToFolder|False|33 +archive1042sDirName|/var/lib/igate/exchange/Archives1042S|33 +attemptInterval|30000|33 +autoInPkgReportOffload|False|33 +autoLoadCrlsTime||33 +automaticcalyLoadCrls|False|33 +autoUpdateTransitMember|False|33 +checkReceivedPackageNsdSign|False|33 +checkReceivedPackageSenderSign|False|33 +cryptography.clientCertificateSerialNumber||33 +cryptography.pincode||33 +cryptography.profile|My|33 +cryptography.type|GOST|33 +dir|/var/lib/igate/exchange|33 +enable|True|33 +enable1042ReportProcessing|True|33 +enableAutoResponse|True|33 +enableLockFile|True|33 +errorDirName|ERRORS|33 +fetchInterval|00:01:00|33 +fetchThreadCount||33 +forceCryPackageEncryption||33 +generateReceivedPackageInfo|True|33 +generateSentPackageInfo|False|33 +ignorePackageDirectoryStructure|False|33 +inboxDirName|INBOX|33 +inPkgReportDirectory||33 +inPkgReportOffloadInterval||33 +isIncomingEnabled|True|33 +isOutgoingEnabled|True|33 +isTransitTerminalChannel|False|33 +loadOldMessagesDepth|3|33 +maxPackagesPerJob||33 +maxPartSize|500|33 +monitoringThreshold|00:00:10|33 +moveReceiptsToSentFolder|False|33 +nsdCertificateSerialNumbers||33 +outboxDirName|OUTBOX|33 +pkiDecryptMode||33 +pkiEncryptMode||33 +pkiSignMode||33 +pkiVerifyMode||33 +receiveProcThreadCount||33 +RenameOutgoingFiles|True|33 +repositoryCode|MC0079200000|33 +sendAttempts|3|33 +sendProcThreadCount||33 +sentDirName|SENT|33 +updateTransitMemberListTime||33 +useDirectories|True|33 +wslEndpoint|https://gost-t3.nsd.ru/onyx-ms/OnyxEdoWSService/OnyxEdo|33 diff --git a/deploy/license/README.md b/deploy/license/README.md new file mode 100644 index 0000000..697ae72 --- /dev/null +++ b/deploy/license/README.md @@ -0,0 +1,71 @@ +# Лицензирование Bridge-and-Join-s (#19) + +Годовые лицензии с офлайн-проверкой по подписи Ed25519 + опциональный +онлайн-отзыв. + +## Компоненты + +- `internal/license` — формат лицензии + подпись/проверка (offline). +- `cmd/bj-license` — издательский CLI: keygen, issue, verify. +- `cmd/bj-license-server` — онлайн-реестр отзывов (revocation). +- Интеграция в bj-server: `internal/lkgateway/licensecheck.go` — проверка + лицензии + гейт обновлений; UI раздел «Лицензия». + +## Модель + +Лицензия — самодостаточный подписанный токен: `payload.signature.keyid`. +bj-server проверяет подпись зашитым публичным ключом и срок **офлайн** — +работает без связи с сервером. Online-сервер нужен только для отзыва. + +**Гейт обновлений:** если лицензирование включено (есть публичный ключ), +авто-обновление (#20) выполняется только при валидной лицензии с фичей +`updates`. Без лицензирования (публичный ключ не зашит) — открытый режим, +гейты не действуют (бесплатная редакция / разработка). + +## Издателю + +```bash +# 1. Ключи лицензий (однократно; приватный — в секрете!) +bj-license keygen -out ./keys/license +# публичный base64 — зашить в bj-server (DefaultLicensePublicKey) + +# 2. Выпустить годовую лицензию клиенту +bj-license issue -tenant "ООО Ромашка" -plan pro -days 365 \ + -features updates,web-cabinet -key ./keys/license.priv -keyid main +# → выводит ключ payload.signature.keyid — отдать клиенту + +# 3. Проверить +bj-license verify -key-file license.key -pub ./keys/license.pub +``` + +Планы: `free` (без фич), `pro` (перечисленные features), `enterprise` +(всё включено). Фичи: `updates`, `web-cabinet`, … + +## Клиенту (on-prem bj-server) + +Админ → Настройка → **Лицензия** → вставить ключ → «Активировать». +Проверка офлайн; статус (организация, план, срок, обновления) виден сразу. + +## Онлайн-отзыв (опционально) + +```bash +bj-license-server --addr :8091 --revoked /var/lib/bj-license/revoked.json +``` + +`revoked.json` — JSON-массив отозванных license ID: + +```json +["28db4973-fde8-434c-b102-e83623eede2c"] +``` + +`GET /v1/check?id=` → `{"revoked":true|false}`. Перечитывается раз в +минуту. В проде заменить файл на PostgreSQL + admin API выпуска/отзыва. + +## Зашивка публичного ключа в релиз + +```bash +go build -ldflags "\ + -X .../lkgateway.DefaultLicensePublicKey= \ + -X .../lkgateway.DefaultUpdatePublicKey= \ + -X .../lkgateway.BuildVersion=1.0.0" -o bj-server ./cmd/bj-server/ +``` diff --git a/deploy/linux/install-validata.sh b/deploy/linux/install-validata.sh new file mode 100755 index 0000000..37664b4 --- /dev/null +++ b/deploy/linux/install-validata.sh @@ -0,0 +1,388 @@ +#!/usr/bin/env bash +# install-validata.sh — установка АПК «Валидата Клиент L» под bj-crypto. +# +# Поддерживаемые ОС: +# - Debian 11 / 12 (основная, бесплатная) +# - Astra Linux SE 1.7 (платная, для регуляторно-обязанных) +# - Astra Linux CE 1.8 (бесплатная) +# - Ubuntu 22.04 / 24.04 (с предупреждением) +# +# Что делает: +# 1. Ставит зависимости (pcscd, libpcsclite, libgtk-3, libldap, p7zip, execstack) +# 2. Ставит zpki + zsdk deb-пакеты Валидаты +# 3. execstack -c libvdcsp.so (исправление GNU_STACK с RWE на RW) +# 4. Создаёт системного пользователя bj (если ещё нет) +# 5. Кладёт 5 systemd drop-ins (pcscd no-autoexit + 3×bj-crypto + 1×bj-server) +# 6. Создаёт /opt/Validata/VDCSP/etc/spki.ini (Валидата с ним капризничает) +# 7. Дописывает заголовочную секцию в pki1.conf +# 8. Включает pcscd в режиме always-on (без socket-активации) — Валидата +# ожидает постоянно живой демон, иначе ловит 0x8010001D +# 9. Ставит udev-rule + systemd-mount unit для авто-mount USB-флешек с .vdk +# в /var/lib/bj/usb/