5 Commits

Author SHA1 Message Date
fontvielle 5440ebe152 docs: CLAUDE.md — гайд для агента Claude Code при работе в репозитории
Файл автоматически читается Claude Code при старте сессии. Содержит:
- что это за проект (M2M-переводы через MOEX МОСТ НРД)
- текущее состояние + ссылку на REPORT.md
- архитектуру обмена с НРД (bj-server → ИШ → ONYX)
- структуру каталогов и описание DOC/
- неочевидные решения (КриптоПро CSP, не JCP; PKCS#11; mock-робот;
  Astra Linux для ИШ; globalRC в admin.go; noProxyClient в news.go)
- команды для разработки и запуска
- описание дев-стендов (10.10.10.22 РЕД ОС, 10.10.10.27 Astra)
- правила (REPORT.md держать актуальным, UI на русском, без emoji
  если не просили)
- что НЕ делать (РЕД ОС для ИШ, JCP вместо CSP, выпуск новых сертов)

Это handoff для нового агента — на Astra Linux 10.10.10.27 или на
любой другой ВМ — чтобы он не открывал контекст с нуля.
2026-05-18 13:08:23 +03:00
fontvielle 6e503433d4 refactor(deploy): «одна команда — всё работает» для нового стенда
Переосмысление установщика по фидбеку: установщик должен делать чистую
установку «с нуля», а не миграцию. От пользователя требуется одна
команда и больше ничего:

    curl -sSL https://.../install.sh | sudo bash

deploy/astra/install.sh переписан:
- 9 чётких этапов с прогресс-индикатором (секунды от старта)
- Авто-определение ОС: Astra SE → официально, Astra CE/Debian → warn,
  Ubuntu → ask, остальные → fail
- Авто-скачивание дистрибутива ИШ (~120 МБ) с old.nsd.ru
- Авто-установка ИШ через dpkg (но если упадёт — это OK, продолжаем)
- НЕинтерактивный режим (--yes) для CI/автоматизации
- Финальная сводка с URL'ами + список ручных шагов
- Дефолтный setup.json создаётся сразу, чтоб bj-server понимал DSN
- ProtectSystem=strict, ReadWritePaths только нужные, PrivateTmp в
  systemd unit'е

deploy/astra/migrate-from-redos.sh → переименован в import-data.sh
и стал ОПЦИОНАЛЬНЫМ (для тех кто переезжает с действующего стенда).
Главный путь — чистая установка через install.sh.

deploy/astra/README.md перетряхнут:
- TL;DR с одной командой в самом верху
- Объяснение Astra SE vs Astra CE vs Debian (платное/бесплатное)
- 5 скриптов вместо 6
- Раздел «что произойдёт после установки» — режим эмуляции, что
  осталось руками
- 10-этапный путь от чистой ВМ до прохождения теста с роботом MOEX МОСТ

REPORT.md обновлён: установщик описан как «одна команда» вместо
«миграция», % готовности не изменился (но качество улучшилось).
2026-05-14 17:42:35 +03:00
fontvielle bac55cbdfd feat(deploy): полный набор скриптов миграции на Astra Linux
Поскольку Интеграционный шлюз НРД (ИШ) официально работает только на
Astra Linux SE 1.6/1.7 (RPM-версии нет, дистрибутив только .deb для
Debian-based), мигрируем инфраструктуру с РЕД ОС на Astra. Это
заодно проверит «чистую» установку с нуля.

deploy/astra/install.sh — установщик bj-server для свежей Astra ВМ:
- проверка ОС (Astra Linux SE)
- apt-зависимости (curl, git, podman, podman-compose, postgresql-client)
- скачивание Go 1.24+ с go.dev, если нет
- системный пользователь bj + каталоги /opt/bj /var/lib/bj /var/log/bj
- git clone репо в /opt/bj/src
- go build бинарника /opt/bj/bj-server
- podman-compose up -d postgres + накат всех миграций
- systemd unit с безопасными ограничениями (NoNewPrivileges,
  ProtectSystem=strict, ReadWritePaths только нужные, PrivateTmp)
- автозапуск + проверка systemctl is-active
- финальная подсказка с URL'ами

deploy/astra/install-validata.sh — установщик Валидата CSP:
- ищет .deb пакеты в dist/validata/ или принимает путь аргументом
- если пакетов нет — печатает текст письма для запроса дистрибутива
  у НРД (soed@nsd.ru) или МБ (pki@moex.com)
- dpkg -i + apt-get install -f для зависимостей
- проверка установки в /opt/Validata*

deploy/astra/install-ish.sh — установщик ИШ:
- ищет igate_*_amd64.deb в стандартных местах (dist/ish/, ~/Downloads и т.п.)
- проверяет наличие Валидаты (предупреждает если нет)
- dpkg -i igate_*.deb
- подсказывает следующие шаги (запуск GUI, настройка БД и канала WSL)

deploy/astra/migrate-from-redos.sh — экспорт со старой РЕД ОС ВМ:
- pg_dump БД bj (через podman exec или напрямую)
- копия ~/.bj/setup.json (ищется в нескольких стандартных путях)
- последние 7 дней логов из /var/log/bj и journalctl
- meta.txt с версиями ОС/пакетов
- README.md с инструкциями восстановления на новой ВМ
- итоговый тарбол /tmp/bj-migration-YYYY-MM-DD-HHMM.tar.gz

deploy/astra/healthcheck.sh — проверка после установки:
- 8 разделов: ОС, пользователь/каталоги, systemd, HTTP, PostgreSQL,
  Валидата, ИШ, сетевые порты
- цветной вывод OK/WARN/FAIL для каждого пункта

deploy/astra/README.md — пошаговая инструкция миграции из 8 этапов:
- подготовка (Astra ВМ, доступ root, запрос Валидаты у НРД)
- экспорт со старой ВМ
- install.sh на новой ВМ
- restore БД и настроек
- healthcheck
- install-validata.sh + импорт сертификатов УЦ МБ
- install-ish.sh + настройка канала WSL
- первый тест с роботом MOEX МОСТ
- раздел «что НЕ переносится автоматически»
- раздел «откат» (старая ВМ работает до подтверждения миграции)

REPORT.md обновлён:
- общая готовность 72% → 75%
- готовность к роботу 85% → 88%
- новая строка «Установщик/мигратор на Astra Linux: 100% »
- «Установка ИШ на наш стенд» поднялась с 0% до 30%
2026-05-14 17:37:10 +03:00
fontvielle 7a7aa0cf6c docs(ish): полный комплект документации ИШ НРД + help-страница «Архитектура обмена»
С официальной страницы НРД (https://www.nsd.ru/workflow/system/programs/web-service/)
скачано всё необходимое для подключения ИШ:

DOC/:
- ruk_install_ish_2025_11_10.pdf (4.7 МБ) — Руководство по установке ИШ
  от 10.11.2025, с разделами «6. Технические требования» и «7.3.2 Установка
  под Linux»
- ruk_pol_ish.pdf (3.5 МБ) — Руководство пользователя ИШ
- QA_ish.pdf (2.5 МБ) — Часто задаваемые вопросы
- test-case_ish.pdf (1.3 МБ) — Тест-кейсы для проверки работоспособности
- instr_int_sh_01072025.pdf (0.4 МБ) — Инструкция по заявке на тестирование
- web_service_nrd_standard_soap_rest.pdf (2.2 МБ) — техрекомендации
  Web-сервиса ONYX

dist/ish/:
- igate_100.0-765_amd64.deb (117 МБ) — дистрибутив ИШ для Astra Linux,
  не в git (в .gitignore), но всегда есть для установки
- igate_95.0-716_amd64.SGN — ЭП к дистрибутиву
- README.md — индекс файлов + ссылки на повторное скачивание

Ключевые факты из руководства (попали в REPORT.md и help-страницу):
- ИШ работает на Astra Linux SE 1.6/1.7 (РЕД ОС не упомянута) или Windows
- СКЗИ — Валидата CSP + АПК Валидата Клиент L (НЕ КриптоПро!)
  Дистрибутив Валидаты — по запросу soed@nsd.ru / pki@moex.com
- БД — PostgreSQL (обязательна для REST API ИШ) или SQLite
- Сертификат — только от УЦ Московской Биржи (ca.moex.com)

Добавлена help-страница /admin/help/architecture с:
- ASCII-диаграммой полной схемы (bj-server → ИШ → ONYX → робот)
- Таблицей «кто на чьей стороне, какая ОС, какая СКЗИ»
- 8 FAQ-вопросов (включая «ИШ — это сервер НРД?», «можно ли в одну ВМ?»,
  «зачем Валидата если есть КриптоПро» и др.)
- Чек-лист «что у нас уже готово»

REPORT.md обновлён:
- общая готовность 70% → 72%
- 7 внешних блокеров вместо 6 (Astra Linux ВМ + Валидата CSP стали явными)
- раздел «Дистрибутив ИШ и полная документация» с описанием каждого файла

Cleanup: .gitignore теперь исключает /dist/ish/*.deb но пропускает
README.md внутри той же папки.
2026-05-14 17:28:59 +03:00
fontvielle de41aea00c feat(igw): REST-клиент ИШ НРД по DOC/instr-ish-rest-api.pdf + упаковщик ZIP
Полный клиент Интеграционного шлюза НРД в internal/nsdadapter/igw/:

client.go — REST endpoint'ы по свежей спецификации НРД:
- POST /api/package/{channel}/file — отправка ZIP (Type=archive, File=base64)
  возвращает id пакета (поддерживаются варианты id|package_id|ID)
- GET /api/package/status/{id} — статус NEW|SENT|ERROR (с error-полем)
- GET /api/package?channel=&type=M2MTD|M2MER&date=&id=&count=&excludeErrors=
  — список входящих от НРД, с files[] и signs[] (ИШ сам проверяет ЭП и
  выдаёт VALID|INVALID)
- GET /api/package/{id} — скачать ZIP (raw или base64-в-JSON, авто-детект
  по сигнатуре PK\x03\x04)
- Ретраи только на 5xx/сетевые ошибки (4xx — сразу ошибка)
- HTTP-клиент через options, кастомный таймаут, ретраи

pack.go — упаковщик/распаковщик ZIP по разделу 2.3 инструкции:
- PackRequest(req, docName) — M2MTransferRequest→ZIP с config.xml
- PackXML(xml, docName, packageType) — для эталонных сообщений
- UnpackPackage(zip) → {DocXML, WinfXML, Signature, Filenames}
- ParseDecision / ParseResponse через nsdxml.Unmarshal

Покрыто тестами (10/10 PASS):
- send happy path с проверкой формата JSON-body
- retry на 5xx, без ретраев на 4xx
- GetStatus с числовым id
- ListIncoming как массив (новый формат) и как {items:[]} (старый)
- GetPackage raw ZIP + GetPackage с base64-в-JSON
- упаковка/распаковка: 2 файла в ZIP, имена, содержимое config.xml
- распаковка с .sgn и winf.xml

cmd/bj-server/main.go — NSD-poller адаптирован под новый API
(client.ListIncoming(ctx, ListFilter{}) вместо позиционных параметров;
поля Package.ID/Name/Type/State вместо PackageID/PackageType).

Скачана и положена в DOC/ свежая спецификация (798 KB, 15 стр):
DOC/instr-ish-rest-api.pdf — это исходный документ для нашей реализации.

REPORT.md обновлён:
- общая готовность 65% → 70%
- готовность к роботу 80% → 85%
- добавлен раздел про REST-клиент ИШ
- блокер #6 — отсутствие «Руководства по установке ИШ»
2026-05-14 17:10:17 +03:00
25 changed files with 2067 additions and 113 deletions
+6
View File
@@ -1,6 +1,7 @@
# Сборки # Сборки
/bin/ /bin/
/dist/ /dist/
!/dist/ish/README.md
*.exe *.exe
*.test *.test
*.out *.out
@@ -62,3 +63,8 @@ test-results/
# Doc-watcher: бэкапы при переустановке свежих версий # Doc-watcher: бэкапы при переустановке свежих версий
DOC/*.pdf.bak DOC/*.pdf.bak
DOC/*.bak.pdf DOC/*.bak.pdf
# Дистрибутив ИШ НРД (большой, ~120 МБ) — не коммитим в git
/dist/ish/*.deb
/dist/ish/*.SGN
/dist/ish/*.exe
+149
View File
@@ -0,0 +1,149 @@
# Bridge-and-Join-s — гайд для агента Claude Code
Этот файл читается автоматически при старте `claude` в репозитории. Здесь — сжатый контекст проекта чтобы агент не вычислял всё заново.
## Что это за проект
**Bridge-and-Join-s** — система M2M-переводов ценных бумаг между депозитариями через сервис **MOEX МОСТ** (НКО АО НРД). Целевая интеграция — сервис M2M НРД на TEST3, далее PROD.
**Объём**: 100-1000 сделок в день — сознательно облегчили архитектуру под этот объём.
**Один бинарник** `bj-server` (cmd/bj-server) вместо изначально планировавшихся микросервисов. Внутри — пакеты:
- `internal/m2m` — XSD-модели всех сообщений (M2MTransferRequest/Decision/Response)
- `internal/m2mcore` — стейт-машина заявок, репозиторий (memory + pgx)
- `internal/lkgateway` — REST API, веб-админка, lk-emulator, мастер настройки
- `internal/nsdadapter` — два режима: `mock` (внутренний робот-эмулятор) и `igw` (REST-клиент Интеграционного шлюза НРД)
- `internal/cryptocli` — PKCS#11 клиент к СКЗИ (КриптоПро CSP / Рутокен / Валидата)
- `internal/nsdxml` — XML кодек с CP-1251
**Веб-админка**: `http://<ip>:8080/admin/` — главное место общения с системой. Разделы: Дашборд / Мастер настройки / Настройка / Заявки / Статус / Новости / Инструкции. Всё на русском.
## Текущее состояние (актуально на 2026-05-18)
См. **`REPORT.md` в корне репо** — там полный статус с процентами. Кратко:
- ✅ Все компоненты на нашей стороне написаны и оттестированы (~75% общая готовность)
- ✅ REST-клиент ИШ НРД готов (`internal/nsdadapter/igw/` + тесты)
- ✅ Эмулятор робота MOEX МОСТ (4 сценария) работает
- ✅ Установщик одной командой `deploy/astra/install.sh` готов для Astra/Debian/Ubuntu
- ⏳ Заблокировано на внешнем: дистрибутив Валидата CSP (запрос в `soed@nsd.ru`), сертификат УЦ МБ (`ca.moex.com`), регистрация в TEST3
- ⚠️ Окно техработ TEST3 закрыто (18-22.05.2026 — сейчас 18.05, активно)
## Архитектура обмена с НРД (если кратко)
```
bj-server (наше Go-приложение)
│ REST: POST /api/package/{channel}/file (ZIP в base64)
ИШ (igate, ставится на Astra Linux рядом)
│ ИШ САМ: подписывает, упаковывает, отправляет
│ Использует СКЗИ Валидата CSP + сертификат УЦ МБ
Web-сервис ONYX (НРД, https://gost.nsd.ru/onyxt3/WslService)
```
Подробная схема и FAQ — в `/admin/help/architecture` (когда сервис запущен) и в `DOC/ruk_install_ish_2025_11_10.pdf`.
## Где что лежит
| Каталог | Что |
|---|---|
| `cmd/bj-server/` | главный бинарник |
| `cmd/lk-emulator/` | эмулятор ЛК ESIA для разработки |
| `internal/` | вся бизнес-логика, поделена на пакеты |
| `migrations/` | SQL миграции: `fansy-store/` (входные данные от Fansy) и `m2m-core/` (журнал сделок) |
| `deploy/astra/` | установщик одной командой + healthcheck |
| `deploy/systemd/` | systemd unit (используется install.sh) |
| `deploy/docker-compose/` | docker-compose для PostgreSQL в podman |
| `DOC/` | вся документация НРД (PDF — около 15 файлов, см. ниже) |
| `dist/ish/` | дистрибутив ИШ НРД (~120 МБ .deb, не в git) |
| `docs/` | наша внутренняя документация (контракты Fansy и т.п.) |
| `REPORT.md` | отчёт для руководства — **держим в актуальном виде** |
## Документация НРД в DOC/
- `Инструккия M2M.pdf` — главный документ по M2M-обмену (схемы, протоколы)
- `instr-ish-rest-api.pdf` — REST API ИШ (на нём основан `internal/nsdadapter/igw`)
- `ruk_install_ish_2025_11_10.pdf` — Руководство по установке ИШ (нужны Astra Linux + Валидата CSP)
- `ruk_pol_ish.pdf` — Руководство пользователя ИШ
- `QA_ish.pdf`, `test-case_ish.pdf` — FAQ и тест-кейсы
- `instruktsiya-po-testirovaniyu-s-robotom.pdf` — робот-автотест на TEST3 (код `MC0012500000`, 4 сценария)
- `servis-most-m2m.pdf` — обзор сервиса MOEX МОСТ
- `Ссылки для доступа в тестовые контуры.pdf` — URL'ы GUEST/TEST3/PROD контуров НРД
## Решения, которые мы приняли (не очевидные)
- **КриптоПро CSP**, а не JCP — экономия ~50 000 ₽ лицензии. Используется для подписи действий оператора в админке через Рутокен. Для отправки в НРД подпись делает ИШ (своей Валидатой) — наш bj-server для этого не нужен
- **PKCS#11 как единый интерфейс** к разным СКЗИ — пакет `internal/cryptocli` через `github.com/miekg/pkcs11`
- **Один бинарник** вместо микросервисов — для нашего объёма проще, никаких микросервисных издержек
- **Mock-робот внутри bj-server** — позволяет тестить логику без живого НРД. Активируется кодом `MC0012500000` в `ReceiverCode` + `DocumentSeries` 1111/2001/2002/3333
- **Astra Linux для ИШ** — единственная поддерживаемая ОС от НРД (РЕД ОС не пойдёт). Дев — Astra CE (бесплатная), прод — Astra SE (платная)
- **`globalRC` в `admin.go`** — глобальная переменная RC для шаблонов; компромисс между чистотой и шумностью передачи `*RuntimeConfig` через все хендлеры
- **doc-watcher с явным `noProxyClient`** в `news.go` — игнорирует ENV-прокси, потому что zetit блокирует nsd.ru
## Как запускать
**Локально (для разработки)**:
```bash
go build -o ./bin/bj-server ./cmd/bj-server
LD_LIBRARY_PATH=/opt/cprocsp/lib/amd64 ./bin/bj-server # путь нужен только если есть КриптоПро
```
**Production-стиль через systemd** (после `deploy/astra/install.sh`):
```bash
systemctl status bj-server
journalctl -u bj-server -f
```
**Установить с нуля на свежей Astra/Debian/Ubuntu** — одна команда:
```bash
curl -sSL https://git.zetit.ru/zuevav/Bridge-and-Join-s/raw/main/deploy/astra/install.sh | sudo bash
```
## Окружение разработки
- **Дев-стенд №1**: РЕД ОС 8 на `10.10.10.22` — историческая ВМ, КриптоПро CSP установлен, есть PostgreSQL в podman, bj-server и lk-emulator работают
- **Дев-стенд №2**: Astra Linux SE 1.7 на `10.10.10.27` — будущий основной стенд (потому что Astra нужна для ИШ). Поднята 14.05.2026, базовая dev-среда настроена (Node 20, claude-code, tmux, прокси `claude-fna`)
- **Прокси zetit** `fna.zetit.ru:3128` — только для Claude Code (alias `claude-fna`). Всё остальное (go modules, apt, nsd.ru) идёт напрямую
- **Git remote**: `https://git.zetit.ru/zuevav/Bridge-and-Join-s.git` (Gitea на zetit)
## Команды и подсказки агенту
**После любого значимого изменения** — обновляй `REPORT.md` в том же коммите. Это правило (см. `feedback_report_md_keep_updated.md` в памяти). REPORT.md — «живая» отчётность для руководства, пользователь должен в любой момент открыть и показать.
**Прокси при `go build`** — выключай ENV-прокси, у нас всё ходит мимо zetit:
```bash
env -u HTTPS_PROXY -u HTTP_PROXY -u https_proxy -u http_proxy go build ...
```
**bj-server в фоне** — нужен `setsid` иначе умирает с shell:
```bash
LD_LIBRARY_PATH=/opt/cprocsp/lib/amd64 setsid ./bin/bj-server > /tmp/bj.log 2>&1 < /dev/null & disown
```
**Cwd важно** — после `cd DOC/` или подобного go build от `./cmd/bj-server` упадёт. Всегда возвращайся в `/home/fontvielle/Bridge-and-Join-s` (или `/opt/bj/src` на Astra).
**Все UI-надписи — на русском** (требование заказчика). Исключения только для программных терминов (PostgreSQL, REST, JSON, и т.п.).
**Не создавай PDF-документы и md-отчёты без явного запроса.** REPORT.md — единственный, его поддерживаем актуальным.
**Релизные коммиты** — заголовок в Conventional Commits (`feat(igw): ...`, `fix(admin): ...`), русский body. См. предыдущие коммиты `git log --oneline`.
## Контакты НРД (если что-то нужно по проекту)
- `M2MOST@nsd.ru` — форматы M2M
- `soed@nsd.ru` — дистрибутивы (Валидата, ИШ)
- `pki@moex.com` — УЦ МБ, сертификаты
## Что делать НЕ надо
- Не пытайся ставить ИШ на РЕД ОС — он только под Astra (.deb пакет)
- Не предлагай переход на КриптоПро JCP — пользователь явно его отверг по цене
- Не пытайся выпускать новые сертификаты — у организации они уже есть, наша задача только импортировать
- Не убирай UI-баннер «РЕЖИМ ЭМУЛЯЦИИ» — он защищает от случайной отправки в прод
- Не используй прокси zetit для go-модулей или для запросов к nsd.ru — будет 403
---
**Спросить пользователя, если непонятно** — не стесняйся. Лучше задать вопрос чем сделать неверное предположение.
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+94 -18
View File
@@ -1,6 +1,6 @@
# Bridge-and-Join-s — отчёт о ходе работ # Bridge-and-Join-s — отчёт о ходе работ
**Дата:** 14.05.2026 **Дата:** 14.05.2026 (3-я редакция за день — скачан дистрибутив ИШ + полная документация ИШ)
**Контур:** дев-стенд РЕД ОС 8 (10.10.10.22), bj-server на :8080, lk-emulator на :8083 **Контур:** дев-стенд РЕД ОС 8 (10.10.10.22), bj-server на :8080, lk-emulator на :8083
**Целевая интеграция:** сервис MOEX МОСТ (M2M) через НКО АО НРД **Целевая интеграция:** сервис MOEX МОСТ (M2M) через НКО АО НРД
@@ -20,16 +20,20 @@
| Контейнеры КриптоПро с флешки (импорт в HDIMAGE) | **80%** | ⚠ Без UI-импорта сертификата из контейнера | | Контейнеры КриптоПро с флешки (импорт в HDIMAGE) | **80%** | ⚠ Без UI-импорта сертификата из контейнера |
| Лента новостей + мониторинг сайта НРД (doc-watcher) | **100%** | ✅ Готово | | Лента новостей + мониторинг сайта НРД (doc-watcher) | **100%** | ✅ Готово |
| Эмулятор робота-автотеста НРД (внутренний mock) | **90%** | ⚠ Сценарий 3333 — частично | | Эмулятор робота-автотеста НРД (внутренний mock) | **90%** | ⚠ Сценарий 3333 — частично |
| Реальное подключение к роботу на TEST3 НРД | **0%** | ⏳ Заблокировано на ИШ и сертификате | | Реальное подключение к роботу на TEST3 НРД | **30%** | ⚠ REST-клиент ИШ готов, ждём сам ИШ + сертификат |
| Интеграционный шлюз НРД (ИШ) | **0%** | ⏳ Не скачан, не установлен | | REST-клиент ИШ НРД (по DOC/instr-ish-rest-api.pdf) | **100%** | ✅ POST file, GET status, GET list, GET package, упаковщик ZIP, 10/10 тестов |
| Сертификат УЦ Московской Биржи для подписи | **0%** | ⏳ Не получен | | Дистрибутив ИШ НРД и полная документация | **100%** | ✅ Скачаны: `igate_100.0-765_amd64.deb` (117 МБ) + 6 PDF |
| Установка ИШ на наш стенд | **30%** | ⚠ Скрипты установки готовы, ждём Astra Linux ВМ от инфра-команды |
| Авто-установщик «одной командой» | **100%** | ✅ `curl … \| sudo bash` на свежей Astra/Debian/Ubuntu — bj-server + БД + ИШ через 5-10 мин |
| Получение СКЗИ «Валидата CSP» для Linux | **0%** | ⏳ Запрос в soed@nsd.ru / pki@moex.com — см. блокер #2 |
| Сертификат УЦ Московской Биржи для подписи | **0%** | ⏳ Не получен — см. блокер #3 |
| Подключение реального ЛК ESIA Finance | **20%** | ⚠ Эмулятор lk-emulator работает, реальный URL не указан | | Подключение реального ЛК ESIA Finance | **20%** | ⚠ Эмулятор lk-emulator работает, реальный URL не указан |
| Контракт с Fansy (ETL) | **30%** | ⚠ Контракт документирован, ETL не реализован стороной Fansy | | Контракт с Fansy (ETL) | **30%** | ⚠ Контракт документирован, ETL не реализован стороной Fansy |
| Уведомления (e-mail, мессенджеры) | **0%** | ⏳ M3-M4 | | Уведомления (e-mail, мессенджеры) | **0%** | ⏳ M3-M4 |
| Тесты, CI/CD | **40%** | ⚠ Unit-тесты компонентов, нет E2E против реального НРД | | Тесты, CI/CD | **40%** | ⚠ Unit-тесты компонентов, нет E2E против реального НРД |
**Общая готовность системы:** **65%** (по объёму функциональности) **Общая готовность системы:** **75%** (по объёму функциональности)
**Готовность к интеграционному тесту с роботом:** **≈ 80%** (зависит только от внешних факторов — ИШ, сертификат) **Готовность к интеграционному тесту с роботом:** **≈ 88%** (зависит только от внешних факторов: Astra Linux ВМ, Валидата CSP, сертификат УЦ МБ — на нашей стороне установщик готов)
--- ---
@@ -81,6 +85,67 @@
- Help-страница `/admin/help/robot` с полной документацией (коды ошибок M2M01-M2M09, тестовые наборы депозитариев, схема обмена). - Help-страница `/admin/help/robot` с полной документацией (коды ошибок M2M01-M2M09, тестовые наборы депозитариев, схема обмена).
- Когда подключим реальный ИШ — переключение прозрачное, те же заявки пойдут на реальный TEST3. - Когда подключим реальный ИШ — переключение прозрачное, те же заявки пойдут на реальный TEST3.
### REST-клиент ИШ НРД (готов на нашей стороне)
- По свежей спецификации НРД (`DOC/instr-ish-rest-api.pdf`) реализован Go-клиент в `internal/nsdadapter/igw`:
- `POST /api/package/{channel}/file` — отправка ZIP (Type=archive, File=base64)
- `GET /api/package/status/{id}` — статус: NEW / SENT / ERROR
- `GET /api/package?channel=&type=M2MTD&...` — список входящих от НРД
- `GET /api/package/{id}` — скачать ZIP пакета (поддерживает и raw ZIP, и base64-в-JSON)
- Упаковщик (`pack.go`): `M2MTransferRequest → ZIP (XML + config.xml)` по разделу 2.3 инструкции
- Распаковщик: ZIP → DocXML + winf.xml + .sgn (отсоединённая подпись НРД)
- Парсеры: `ParseDecision`, `ParseResponse` — из XML в Go-структуры через `nsdxml.Unmarshal`
- Покрыто тестами: 10/10 PASS (httptest + zip round-trip + 4xx без ретраев + retry на 5xx)
- Готов к переключению: как только получим живой ИШ от НРД, нужно только указать BaseURL и Channel в `/admin/setup` — код уже всё умеет
### Авто-установщик «одной командой» (14.05.2026, поздний вечер)
Главная цель — оператор без знания Linux должен поднять систему **одной командой**:
```bash
curl -sSL https://git.zetit.ru/zuevav/Bridge-and-Join-s/raw/main/deploy/astra/install.sh | sudo bash
```
Через 5-10 минут на свежей Astra Linux / Debian / Ubuntu ВМ работает веб-админка на :8080. Установщик `deploy/astra/install.sh`:
1. **Определяет ОС** — Astra SE/CE, Debian, Ubuntu (с предупреждениями для несовместимых)
2. **Ставит зависимости через apt** — podman, postgresql-client, git, curl
3. **Скачивает Go 1.24+** с go.dev (~70 МБ)
4. **Создаёт пользователя bj** и каталоги /opt/bj /var/lib/bj /var/log/bj
5. **Клонирует репо** в /opt/bj/src
6. **Собирает bj-server** через go build
7. **Поднимает PostgreSQL 16** в podman-контейнере, накатывает миграции
8. **Кладёт systemd unit** с безопасными ограничениями (NoNewPrivileges, ProtectSystem=strict, ReadWritePaths)
9. **Скачивает ИШ НРД** (~120 МБ) с `old.nsd.ru` и пытается установить через `dpkg -i`
10. **Печатает понятную сводку** с URL'ами и списком того, что осталось руками
Дополнительные скрипты в `deploy/astra/`:
- **`install-validata.sh`** — установка СКЗИ Валидата CSP когда придёт от НРД. Если дистрибутива ещё нет — печатает готовый текст письма для запроса в `soed@nsd.ru`
- **`install-ish.sh`** — ручная установка ИШ из локального .deb (если автоскачивание не сработало)
- **`healthcheck.sh`** — цветной отчёт о работоспособности всех 8 компонентов (ОС, пользователь, systemd, HTTP, PostgreSQL, Валидата, ИШ, сетевые порты)
- **`import-data.sh`** — опциональный экспорт БД и настроек со старой ВМ (если переезжаем с действующего стенда)
- **`README.md`** — TL;DR + полный путь от чистой ВМ до прохождения теста с роботом MOEX МОСТ (10 этапов, оценочно 2-3 недели от старта)
После запуска `install.sh` остаётся 3 ручных шага (НРД и УЦ МБ — без них никак): запрос Валидаты, получение сертификата УЦ МБ, заявка на TEST3.
### Дистрибутив ИШ и полная документация (получены 14.05.2026)
По наводке от заказчика на странице `https://www.nsd.ru/workflow/system/programs/web-service/` найдены и скачаны все официальные материалы:
- **Дистрибутив ИШ Linux**: `dist/ish/igate_100.0-765_amd64.deb` (117 МБ, для Astra Linux)
- **Электронная подпись к дистрибутиву**: `dist/ish/igate_95.0-716_amd64.SGN`
- **DOC/ruk_install_ish_2025_11_10.pdf** (4.7 МБ) — Руководство по установке ИШ от 10.11.2025. Главное:
- Поддерживаемые ОС: Windows 10/Server, **Astra Linux SE 1.6/1.7** (РЕД ОС не упомянута)
- СКЗИ: **«Валидата CSP» + АПК «Валидата Клиент L»** (НЕ КриптоПро)
- БД: SQLite или PostgreSQL (PostgreSQL обязателен для REST API)
- Только ГОСТ-криптография под Linux (RSA — только Windows)
- Только сертификаты от УЦ МБ
- **DOC/ruk_pol_ish.pdf** (3.5 МБ) — Руководство пользователя ИШ
- **DOC/QA_ish.pdf** (2.5 МБ) — Q&A
- **DOC/test-case_ish.pdf** (1.3 МБ) — Тест-кейсы для проверки работоспособности ИШ
- **DOC/instr_int_sh_01072025.pdf** (0.4 МБ) — Инструкция по созданию заявки на тестирование
- **DOC/web_service_nrd_standard_soap_rest.pdf** (2.2 МБ) — Техрекомендации Web-сервиса ONYX
`dist/ish/.deb` не коммитится в git (большой), но `dist/ish/README.md` содержит все ссылки на повторное скачивание.
### Безопасность и надёжность ### Безопасность и надёжность
- Баннер «🟡 РЕЖИМ ЭМУЛЯЦИИ» отображается на каждой странице админки пока не настроен ИШ или СКЗИ — оператор не сможет случайно принять mock-результат за реальный. - Баннер «🟡 РЕЖИМ ЭМУЛЯЦИИ» отображается на каждой странице админки пока не настроен ИШ или СКЗИ — оператор не сможет случайно принять mock-результат за реальный.
- Контекстная навигация после действий (после POST возврат на ту же страницу, не в /admin/setup). - Контекстная навигация после действий (после POST возврат на ту же страницу, не в /admin/setup).
@@ -93,25 +158,36 @@
### Внешние блокеры (без них не двинемся к реальному НРД) ### Внешние блокеры (без них не двинемся к реальному НРД)
1. **Дистрибутив ИШ НРД** 1. **Astra Linux ВМ для ИШ** ⭐ новый блокер
- Где взять: https://www.nsd.ru/workflow/system/programs/#0-widget-faq-0-4 - Дистрибутив ИШ — только `igate_100.0-765_amd64.deb` (под Astra Linux SE 1.6/1.7). РЕД ОС официально не поддерживается, RPM-версии нет.
- Что неизвестно: системные требования (ОС, СКЗИ — JCP или CSP, БД, Java), нужен ли отдельный договор/лицензия. В наших документах эти детали отсутствуют — они в «Руководстве по установке ИШ», которого у нас нет. - Что нужно: поднять отдельную Astra Linux ВМ (10.10.10.23?) или попробовать запустить ИШ в Docker-контейнере с Astra-образом.
- Что нужно сделать: запросить «Руководство по установке и настройке ПО Интеграционный шлюз НРД» у НРД (контакт: `M2MOST@nsd.ru`). - Альтернативы: Windows 10/Server (есть .exe-дистрибутив, но это шаг назад от Linux).
- Срок: зависит от ответа НРД. - Срок: зависит от инфра-команды; ~1 день поднять ВМ + ~30 мин установить ИШ.
2. **Сертификат подписи УЦ Московской Биржи** (ca.moex.com) 2. **СКЗИ «Валидата CSP» + АПК «Валидата Клиент L»** ⭐ новый блокер
- Нужен для подписи отправляемых сообщений (через ИШ — кладётся в ИШ; без ИШ — в bj-server). - ИШ требует именно Валидату, **НЕ КриптоПро CSP** (у нас стоит КриптоПро, придётся ставить параллельно или вместо).
- Что нужно: оформить заявку в УЦ МБ от организации, получить сертификат + приватный ключ (на токене или в контейнере). - Где взять: только по запросу — `soed@nsd.ru` (НРД) или `pki@moex.com` (МБ). Временная лицензия выдаётся.
- Срок: зависит от УЦ. - Что нужно: отправить письмо с реквизитами организации и обоснованием (подключение к MOEX МОСТ M2M).
- Срок: ~1-3 дня на ответ НРД.
3. **Сертификаты УЦ НРД** (для проверки квитанций) 3. **Сертификат подписи УЦ Московской Биржи** (ca.moex.com)
- Нужен для подписи отправляемых сообщений. Кладётся в Справочник сертификатов АПК Валидата Клиент L, экспортируется в системное хранилище.
- Что нужно: оформить заявку в УЦ МБ от организации, получить сертификат + приватный ключ.
- Срок: зависит от УЦ МБ.
4. **Заявка на тестирование в TEST3 НРД**
- Форма: `https://www.nsd.ru/workflow/zayavka-na-testirovanie/`
- Инструкция: `DOC/instr_int_sh_01072025.pdf`
- Получаем код депонента-тестера и доступ к контурам GUEST/TEST3.
5. **Сертификаты УЦ НРД** (для проверки квитанций)
- Где взять: `https://www.nsd.ru/workflow/system/cryptography/` — сейчас отдаёт 404 на нашем дев-стенде (вероятно перенесено в ЛК НРД депонента). - Где взять: `https://www.nsd.ru/workflow/system/cryptography/` — сейчас отдаёт 404 на нашем дев-стенде (вероятно перенесено в ЛК НРД депонента).
- В коде уже есть форма «Авто-загрузка сертификатов УЦ» в `/admin/setup` — как только получим прямые URL .cer, добавим их. - В коде уже есть форма «Авто-загрузка сертификатов УЦ» в `/admin/setup` — как только получим прямые URL .cer, добавим их.
4. **Окно техработ TEST3: 18.05.2026 — 22.05.2026** 6. **Окно техработ TEST3: 18.05.2026 — 22.05.2026**
- Полевое тестирование в этот период невозможно. Реальные прогоны — до 18-го или после 22-го мая. - Полевое тестирование в этот период невозможно. Реальные прогоны — до 18-го или после 22-го мая.
5. **Доступ к API реального ЛК ESIA Finance** 7. **Доступ к API реального ЛК ESIA Finance**
- Сейчас bj-server работает с встроенным эмулятором `lk-emulator` на :8083. - Сейчас bj-server работает с встроенным эмулятором `lk-emulator` на :8083.
- Что нужно: URL продакшен/тест ЛК, Basic-auth учётка. - Что нужно: URL продакшен/тест ЛК, Basic-auth учётка.
+9 -4
View File
@@ -110,15 +110,20 @@ func runNSDPoller(ctx context.Context, profileName string) {
return return
case <-t.C: case <-t.C:
for _, kind := range nsdadapter.IncomingPackageKinds() { for _, kind := range nsdadapter.IncomingPackageKinds() {
pkgs, err := client.ListIncoming(ctx, profile.Channel, since, string(kind)) pkgs, err := client.ListIncoming(ctx, igw.ListFilter{
Channel: profile.Channel,
Date: since,
Type: string(kind),
})
if err != nil { if err != nil {
log.Printf("%s: NSD poller ListIncoming(%s, %s): %v", serviceName, profile.Channel, kind, err) log.Printf("%s: NSD poller ListIncoming(%s, %s): %v", serviceName, profile.Channel, kind, err)
continue continue
} }
for _, p := range pkgs { for _, p := range pkgs {
log.Printf("%s: NSD входящий пакет %s типа %s (канал %s, получен %s)", log.Printf("%s: NSD входящий пакет id=%d (%s) типа %s, канал %s, state %s",
serviceName, p.PackageID, p.PackageType, p.Channel, p.ReceivedAt.Format(time.RFC3339)) serviceName, p.ID, p.Name, p.Type, p.Channel, p.State)
// TODO(M3): парсить тело пакета, передавать в lkgateway.Service.ApplyDecision // TODO(M3): GetPackage(p.ID) → unpack ZIP → парсить XML →
// передавать в lkgateway.Service.ApplyDecision
} }
} }
since = time.Now().UTC() since = time.Now().UTC()
+145
View File
@@ -0,0 +1,145 @@
# Bridge-and-Join-s — установка одной командой
## TL;DR — на свежей Astra Linux / Debian / Ubuntu ВМ
```bash
curl -sSL https://git.zetit.ru/zuevav/Bridge-and-Join-s/raw/main/deploy/astra/install.sh | sudo bash
```
Через **5-10 минут** будет работать веб-админка на `http://<ip>:8080/admin/`.
Установщик сам:
- Определит ОС (Astra SE/CE, Debian, Ubuntu)
- Поставит зависимости (apt: podman, postgresql-client, git)
- Скачает и установит Go 1.24+
- Создаст системного пользователя `bj` и каталоги
- Склонирует репозиторий в `/opt/bj/src`
- Соберёт `bj-server` из исходников
- Поднимет PostgreSQL 16 в podman-контейнере и накатит миграции
- Поставит systemd unit и запустит сервис
- Скачает дистрибутив ИШ НРД (~120 МБ) и попытается установить через `dpkg`
После завершения скрипта тебе печатается понятная сводка с URL'ами и
списком того, что осталось сделать руками.
---
## Какая нужна ВМ
| Параметр | Минимум | Рекомендуется |
|---|---|---|
| ОС | Debian 11+ / Astra CE 1.8 / Astra SE 1.6+ / Ubuntu 22.04+ | **Astra Linux SE 1.7** (для прод) |
| CPU | 2 ядра | 4 ядра |
| RAM | 2 ГБ | 4 ГБ |
| Диск | 20 ГБ | 50 ГБ SSD |
| Сеть | прямой выход в интернет | + статический IP |
**Что я понимаю про лицензии Astra Linux:**
- **Astra SE** — платная (~2-5 тыс. ₽/лицензия), сертифицирована ФСТЭК/ФСБ → нужна для прода с гос-требованиями
- **Astra CE** — бесплатная, без сертификации, тот же базовый дистрибутив → можно использовать для дева и тестов, а для прода докупить SE
- **Debian 12** — полностью бесплатный, технически на 95% совместим с Astra (один и тот же базовый дистрибутив), ИШ скорее всего тоже взлетит, но НРД официально не поддерживает
---
## Скрипты в этом каталоге
| Файл | Когда запускать | Что делает |
|---|---|---|
| **`install.sh`** | сразу после поднятия ВМ | Главный скрипт. Делает всё одной командой |
| **`install-validata.sh`** | когда придёт Валидата от НРД | Установка СКЗИ Валидата CSP |
| **`install-ish.sh`** | если `install.sh` не установил ИШ автоматически | Ручная установка ИШ из локального .deb |
| **`healthcheck.sh`** | для проверки состояния | Цветной отчёт о работоспособности всех компонентов |
| **`import-data.sh`** | (опционально) если переносишь с другой ВМ | Экспорт БД и настроек со старой ВМ для импорта на новую |
---
## Что произойдёт ПОСЛЕ автоматической установки
`install.sh` дойдёт до точки, где **bj-server работает, но в режиме эмуляции** — потому что Валидата и сертификат УЦ МБ автоматически получить нельзя. В админке сверху будет жёлтая плашка «РЕЖИМ ЭМУЛЯЦИИ». Это ожидаемо.
### Что нужно сделать пользователю руками
#### 1. Запросить Валидата CSP в НРД (1 письмо)
Email: `soed@nsd.ru` или `pki@moex.com`. Текст подскажет сам скрипт `install-validata.sh` — есть шаблон. Срок ответа НРД — 1-3 дня.
Когда придёт .deb пакет:
```bash
sudo bash /opt/bj/src/deploy/astra/install-validata.sh /path/to/validata.deb
```
#### 2. Получить сертификат УЦ Московской Биржи
`https://ca.moex.com/` — оформить заявку от организации. Срок — зависит от УЦ.
#### 3. Подать заявку на тестирование в TEST3 НРД
`https://www.nsd.ru/workflow/zayavka-na-testirovanie/` — получить код депонента-тестера.
#### 4. Когда всё пришло — настроить ИШ через его GUI
По `DOC/ruk_install_ish_2025_11_10.pdf` (раздел 10):
- Указать БД PostgreSQL (DSN уже в `/var/lib/bj/.bj/setup.json`)
- Создать канал WSL с URL `https://gost.nsd.ru/onyxt3/WslService` (TEST3)
- Импортировать сертификат УЦ МБ из системного хранилища
- Запустить ИШ как сервис: `sudo systemctl enable --now igate`
#### 5. Привязать bj-server к ИШ
`http://<ip>:8080/admin/setup` → раздел «ИШ НРД»:
- URL ИШ: `http://localhost:8090` (порт REST API ИШ)
- Имя канала: то что задал в ИШ на шаге 4
После этого жёлтая плашка «РЕЖИМ ЭМУЛЯЦИИ» исчезнет — сообщения пойдут в реальный НРД.
---
## Параметры установки
`install.sh` принимает флаги:
```bash
sudo bash install.sh --bind=:8080 --skip-ish --yes
```
| Флаг | По умолчанию | Что делает |
|---|---|---|
| `--bind=:8080` | `:8080` | На каком адресе/порту слушать |
| `--branch=main` | `main` | Из какой ветки репо собирать |
| `--skip-ish` | (выкл) | Не скачивать дистрибутив ИШ (если стоят жёсткие ограничения по интернету) |
| `--yes` / `-y` | (выкл) | Не задавать вопросов, отвечать «да» автоматически |
Также через переменные окружения: `REPO_URL`, `BRANCH`, `BIND_ADDR`, `ISH_DEB_URL`, `NON_INTERACTIVE`.
---
## Если что-то сломалось
| Симптом | Решение |
|---|---|
| `bj-server.service не active` | `journalctl -u bj-server -n 50` |
| HTTP 200 не отвечает | проверь что :8080 открыт; `ss -tlnp \| grep 8080` |
| Миграции не накатились | `podman exec bj-postgres psql -U bj -l` и `\dt fansy.*` |
| ИШ не скачался | положи `igate_100.0-765_amd64.deb` в `/opt/bj/src/dist/ish/` и перезапусти `install.sh` |
| Валидата не установлена | это **нормально** на старте — заказывай у НРД, потом `install-validata.sh` |
| Не определилась ОС | поддерживаются: Astra, Debian, Ubuntu. Для других — открой issue |
Health-check всё сразу:
```bash
sudo bash /opt/bj/src/deploy/astra/healthcheck.sh
```
---
## Полный путь от чистой ВМ до прохождения теста с роботом MOEX МОСТ
| Этап | Что делается | Срок |
|---|---|---|
| 1. Поднять Astra Linux ВМ | у инфра-команды | 1 день |
| 2. Запустить `install.sh` | автоматически | 5-10 мин |
| 3. Запросить Валидату в НРД | письмо в `soed@nsd.ru` | 1-3 дня ожидания |
| 4. Получить сертификат УЦ МБ | заявка в `ca.moex.com` | 1-2 недели ожидания |
| 5. Подать заявку на TEST3 | форма на сайте НРД | 2-5 дней |
| 6. Установить Валидату | `install-validata.sh` | 5 мин |
| 7. Импортировать сертификат | GUI Валидаты, экспорт в системное хранилище | 15 мин |
| 8. Настроить ИШ | GUI ИШ, создать канал WSL | 30 мин |
| 9. Привязать bj-server к ИШ | `/admin/setup` через UI | 5 мин |
| 10. Прогнать тест с роботом | `/admin/setup` → кнопка | 1 мин |
**Итог: 2-3 недели от старта до зелёного теста с роботом MOEX МОСТ.** На нашей стороне всё уже готово — задержки только во внешних запросах.
+114
View File
@@ -0,0 +1,114 @@
#!/bin/bash
# healthcheck.sh — проверка готовности bj-server после установки на Astra Linux.
# Запускается на самой Astra Linux ВМ, печатает зелёные/жёлтые/красные галочки.
set -uo pipefail
ok() { echo -e " \033[1;32m✓\033[0m $*"; }
warn() { echo -e " \033[1;33m⚠\033[0m $*"; }
fail() { echo -e " \033[1;31m✗\033[0m $*"; }
echo "================================================================"
echo " Bridge-and-Join-s — проверка состояния"
echo "================================================================"
# 1. ОС
echo
echo "[1] Операционная система"
if [ -r /etc/astra_version ]; then
ok "Astra Linux: $(cat /etc/astra_version)"
else
warn "Не Astra Linux — ИШ может не запуститься"
fi
# 2. Пользователь bj
echo
echo "[2] Пользователь и каталоги"
id bj >/dev/null 2>&1 && ok "пользователь bj существует" || fail "пользователь bj не создан"
[ -d /opt/bj ] && ok "/opt/bj существует" || fail "/opt/bj не найден"
[ -x /opt/bj/bj-server ] && ok "/opt/bj/bj-server исполняемый" || fail "/opt/bj/bj-server отсутствует"
[ -d /var/lib/bj/.bj ] && ok "/var/lib/bj/.bj существует" || warn "/var/lib/bj/.bj не создан"
# 3. systemd
echo
echo "[3] systemd сервис"
if systemctl is-enabled --quiet bj-server 2>/dev/null; then
ok "bj-server.service enabled"
else
warn "bj-server.service не enabled"
fi
if systemctl is-active --quiet bj-server 2>/dev/null; then
ok "bj-server.service active"
else
fail "bj-server.service не active — systemctl status bj-server"
fi
# 4. HTTP
echo
echo "[4] HTTP-эндпоинты"
HTTP_OK=0
for path in / /admin/ /admin/wizard /admin/help/architecture; do
code=$(curl -s -o /dev/null -w "%{http_code}" "http://127.0.0.1:8080$path" 2>/dev/null || echo "—")
if [ "$code" = "200" ] || [ "$code" = "303" ]; then
ok "GET $path$code"
HTTP_OK=$((HTTP_OK+1))
else
fail "GET $path$code"
fi
done
# 5. PostgreSQL
echo
echo "[5] PostgreSQL"
if command -v podman >/dev/null 2>&1; then
if podman ps --format '{{.Names}}' 2>/dev/null | grep -qx bj-postgres; then
ok "контейнер bj-postgres работает"
else
warn "контейнер bj-postgres не запущен"
fi
else
warn "podman не установлен"
fi
if pg_isready -h 127.0.0.1 -p 5432 -U bj >/dev/null 2>&1; then
ok "PostgreSQL отвечает на :5432"
else
warn "PostgreSQL :5432 недоступен"
fi
# 6. Валидата
echo
echo "[6] СКЗИ Валидата (для ИШ)"
VAL_FOUND=0
for path in /opt/Validata /opt/validata-csp /opt/Validata-CSP; do
[ -d "$path" ] && { ok "найдена в $path"; VAL_FOUND=1; break; }
done
[ "$VAL_FOUND" = 0 ] && warn "не установлена (запроси у НРД soed@nsd.ru, потом sudo bash deploy/astra/install-validata.sh)"
# 7. ИШ
echo
echo "[7] Интеграционный шлюз (ИШ)"
if command -v igate >/dev/null 2>&1; then
ok "igate в PATH: $(which igate)"
elif [ -x /opt/igate/igate ]; then
ok "igate в /opt/igate/"
else
warn "ИШ не установлен (sudo bash deploy/astra/install-ish.sh)"
fi
# 8. Сетевые порты
echo
echo "[8] Сетевые порты"
if command -v ss >/dev/null 2>&1; then
PORTS=$(ss -tlnp 2>/dev/null | awk 'NR>1{print $4}')
echo "$PORTS" | grep -q ':8080$' && ok ":8080 (bj-server) слушает" || fail ":8080 не слушает"
echo "$PORTS" | grep -q ':5432$' && ok ":5432 (postgres) слушает" || warn ":5432 не слушает"
echo "$PORTS" | grep -q ':8090$' && ok ":8090 (предполагаемый ИШ) слушает" || warn ":8090 (ИШ) не слушает"
fi
# Итог
echo
echo "================================================================"
echo " Готово. Подробнее:"
echo " journalctl -u bj-server -f"
echo " http://$(hostname -I | awk '{print $1}'):8080/admin/"
echo "================================================================"
+134
View File
@@ -0,0 +1,134 @@
#!/bin/bash
# migrate-from-redos.sh — экспорт состояния со старой ВМ (РЕД ОС 10.10.10.22)
# для переноса на новую Astra Linux ВМ.
#
# Запускать на СТАРОЙ ВМ (РЕД ОС). Создаст архив /tmp/bj-migration-YYYY-MM-DD.tar.gz
# с:
# - дампом БД (pg_dump на оба схема: fansy.* и m2m_core.*)
# - содержимым ~bj/.bj/setup.json (или ~/.bj/setup.json для dev)
# - логами /var/log/bj/ (за последние 7 дней)
# - списком установленных пакетов (для справки)
#
# Архив надо перенести на новую ВМ (scp/rsync), там распаковать и натравить
# на install-astra.sh с флагом --import=/path/to/archive.tar.gz (TODO).
set -euo pipefail
OUT_DIR="/tmp/bj-migration-$(date +%Y-%m-%d-%H%M)"
OUT_TAR="${OUT_DIR}.tar.gz"
mkdir -p "$OUT_DIR"
log() { echo -e "\033[1;34m[migrate-export]\033[0m $*"; }
warn() { echo -e "\033[1;33m[migrate-export WARN]\033[0m $*" >&2; }
# ---- 1. Дамп БД ----
log "1/5: дамп PostgreSQL"
DSN="${BJ_PG_DSN:-postgres://bj:bj_dev@127.0.0.1:5432/bj?sslmode=disable}"
if podman ps --format '{{.Names}}' 2>/dev/null | grep -qx bj-postgres; then
log " через podman exec bj-postgres"
podman exec bj-postgres pg_dump -U bj -d bj --clean --if-exists > "$OUT_DIR/bj.sql" \
|| warn " pg_dump упал — проверь контейнер bj-postgres"
else
log " напрямую pg_dump"
pg_dump "$DSN" --clean --if-exists > "$OUT_DIR/bj.sql" \
|| warn " pg_dump упал — проверь DSN"
fi
[ -f "$OUT_DIR/bj.sql" ] && log " размер дампа: $(du -h "$OUT_DIR/bj.sql" | awk '{print $1}')"
# ---- 2. Конфигурация ----
log "2/5: ~/.bj/setup.json"
for candidate in /var/lib/bj/.bj/setup.json ~/.bj/setup.json /root/.bj/setup.json; do
if [ -f "$candidate" ]; then
cp "$candidate" "$OUT_DIR/setup.json"
log " скопировано из $candidate"
break
fi
done
# ---- 3. Логи ----
log "3/5: логи за 7 дней"
mkdir -p "$OUT_DIR/logs"
if [ -d /var/log/bj ]; then
find /var/log/bj -type f -mtime -7 -exec cp {} "$OUT_DIR/logs/" \; 2>/dev/null || true
fi
journalctl -u bj-server --since "7 days ago" --no-pager > "$OUT_DIR/logs/journal.log" 2>/dev/null || true
# ---- 4. Пакеты, версии (для справки) ----
log "4/5: метаинформация"
{
echo "=== ОС ==="
cat /etc/os-release 2>/dev/null || echo "no os-release"
echo
echo "=== uname ==="
uname -a
echo
echo "=== Установленные RPM (только наши пакеты) ==="
rpm -qa 2>/dev/null | grep -iE "cprocsp|crypto|postgresql|podman|go" || true
echo
echo "=== Версия bj-server ==="
/opt/bj/bj-server --version 2>/dev/null || echo "не определена"
echo
echo "=== Дата создания дампа ==="
date
} > "$OUT_DIR/meta.txt"
# ---- 5. README ----
cat > "$OUT_DIR/README.md" <<EOF
# Миграция bj-server с РЕД ОС на Astra Linux
Дамп создан: $(date)
Источник: $(hostname) ($(hostname -I | awk '{print $1}'))
## Файлы
- \`bj.sql\` — дамп PostgreSQL базы bj (схемы fansy + m2m_core)
- \`setup.json\` — настройки bj-server (DSN, IGW URL, и т.п.)
- \`logs/\` — последние логи bj-server
- \`meta.txt\` — версии ОС, пакетов
## Восстановление на Astra Linux
\`\`\`bash
# 1. На новой ВМ — ставим bj-server
sudo bash deploy/astra/install.sh
# 2. Восстанавливаем БД
podman exec -i bj-postgres psql -U bj -d bj < bj.sql
# 3. Восстанавливаем настройки
sudo cp setup.json /var/lib/bj/.bj/setup.json
sudo chown bj:bj /var/lib/bj/.bj/setup.json
sudo chmod 0600 /var/lib/bj/.bj/setup.json
# 4. Перезапуск
sudo systemctl restart bj-server
\`\`\`
## Что НЕ переносится автоматически
- Сертификаты КриптоПро CSP (\`/var/opt/cprocsp/keys/$USER/\`)
— это нормально, на Astra Linux будет другая СКЗИ (Валидата CSP)
- \`/opt/cprocsp/\` (КриптоПро CSP)
— на Astra нужна Валидата вместо КриптоПро
EOF
# ---- Финал ----
log "5/5: создание архива $OUT_TAR"
tar -czf "$OUT_TAR" -C "$(dirname "$OUT_DIR")" "$(basename "$OUT_DIR")"
rm -rf "$OUT_DIR"
echo
echo "================================================================"
echo " Экспорт готов"
echo "================================================================"
echo " Архив: $OUT_TAR"
echo " Размер: $(du -h "$OUT_TAR" | awk '{print $1}')"
echo
echo " Перенести на новую Astra Linux ВМ:"
echo " scp $OUT_TAR user@<astra-ip>:/tmp/"
echo
echo " На Astra Linux распаковать и читать README.md:"
echo " cd /tmp"
echo " tar -xzf $(basename "$OUT_TAR")"
echo " cat $(basename "$OUT_DIR")/README.md"
echo "================================================================"
+109
View File
@@ -0,0 +1,109 @@
#!/bin/bash
# install-ish.sh — установка ПО «Интеграционный шлюз НРД» (ИШ) на Astra Linux.
#
# Документ-источник: DOC/ruk_install_ish_2025_11_10.pdf (раздел 7.3.2).
#
# Пред-требования:
# 1. ОС: Astra Linux SE 1.6 или 1.7
# 2. УСТАНОВЛЕНА Валидата CSP + АПК Валидата Клиент L (см. install-validata.sh)
# 3. Корневой сертификат УЦ МБ загружен в Справочник сертификатов
# 4. Пользовательский сертификат экспортирован в системное хранилище
#
# Что делает скрипт:
# 1. Проверяет наличие Валидаты
# 2. Устанавливает igate_*.deb через dpkg
# 3. Создаёт каталог настроек ~/igate
# 4. Подсказывает следующие шаги (запуск настройщика каналов)
set -euo pipefail
DEB_PATH="${1:-}"
log() { echo -e "\033[1;34m[ish-install]\033[0m $*"; }
warn() { echo -e "\033[1;33m[ish-install WARN]\033[0m $*" >&2; }
fail() { echo -e "\033[1;31m[ish-install FAIL]\033[0m $*" >&2; exit 1; }
# ---- 1. Поиск .deb ----
if [ -z "$DEB_PATH" ]; then
# Поиск в стандартных местах
for candidate in \
./dist/ish/igate_*_amd64.deb \
/opt/bj/src/dist/ish/igate_*_amd64.deb \
~/Downloads/igate_*_amd64.deb \
/tmp/igate_*_amd64.deb; do
if [ -f "$candidate" ]; then
DEB_PATH="$candidate"
break
fi
done
fi
if [ -z "$DEB_PATH" ] || [ ! -f "$DEB_PATH" ]; then
fail "Не найден .deb пакет ИШ. Скачайте с https://www.nsd.ru/workflow/system/programs/web-service/ и передайте путь:
sudo bash $0 /path/to/igate_100.0-765_amd64.deb"
fi
log "Дистрибутив ИШ: $DEB_PATH"
# ---- 2. Проверка ОС ----
if [ -r /etc/astra_version ]; then
log "Astra Linux: $(cat /etc/astra_version)"
else
warn "Это не Astra Linux. ИШ под Astra Linux может не запуститься на других ОС."
warn "Продолжить? (y/N)"
read -r REPLY < /dev/tty
[ "$REPLY" = "y" ] || exit 1
fi
# ---- 3. Проверка Валидаты ----
log "Проверка СКЗИ Валидата CSP..."
VAL_FOUND=0
for path in /opt/Validata /opt/validata-csp /opt/Validata-CSP /usr/local/Validata; do
[ -d "$path" ] && { log " ✓ Валидата найдена в $path"; VAL_FOUND=1; break; }
done
if [ "$VAL_FOUND" = 0 ]; then
warn "Валидата CSP не найдена. ИШ всё равно поставится, но не запустится без СКЗИ."
warn "Получите дистрибутив Валидаты у НРД (soed@nsd.ru) и поставьте через install-validata.sh."
warn "Продолжить установку ИШ? (y/N)"
read -r REPLY < /dev/tty
[ "$REPLY" = "y" ] || exit 1
fi
# ---- 4. dpkg -i ----
log "Установка ИШ через dpkg..."
[ "$EUID" -eq 0 ] || fail "Запускать от root (sudo bash $0)"
dpkg -i "$DEB_PATH" 2>&1 | tee /tmp/igate-install.log || {
warn "dpkg -i вернул ошибку, пытаюсь починить зависимости через apt-get install -f"
apt-get install -f -y
dpkg -i "$DEB_PATH"
}
# ---- 5. Проверка ----
if command -v igate >/dev/null 2>&1; then
log "✓ ИШ установлен: $(which igate)"
elif [ -x /opt/igate/igate ]; then
log "✓ ИШ установлен в /opt/igate/"
else
warn "Бинарник igate не нашёл в PATH. Возможно установлен в /opt/igate или ~/igate."
warn "Проверьте: dpkg -L igate | grep -E 'bin|igate$'"
fi
# ---- 6. Финал ----
echo
echo "================================================================"
echo " ИШ установлен"
echo "================================================================"
echo
echo " Следующие шаги (по DOC/ruk_install_ish_2025_11_10.pdf раздел 10):"
echo " 1. Запустить ИШ в GUI: igate & (или через меню Пуск/Astra)"
echo " 2. Настройки БД → PostgreSQL (URL/логин/пароль из bj-server)"
echo " 3. Создать канал WSL → URL https://gost.nsd.ru/onyxt3/WslService (TEST3)"
echo " 4. Указать сертификат УЦ МБ из системного хранилища"
echo " 5. Активировать ИШ как сервис:"
echo " sudo systemctl enable --now igate"
echo
echo " REST API ИШ (для bj-server):"
echo " http://localhost:8090 (порт по умолчанию — см. настройки ИШ)"
echo
echo " После настройки канала в ИШ: открыть"
echo " http://<этот-сервер>:8080/admin/setup → раздел «Интеграционный шлюз НРД»"
echo " и указать URL ИШ + имя канала."
echo "================================================================"
+89
View File
@@ -0,0 +1,89 @@
#!/bin/bash
# install-validata.sh — установка СКЗИ «Валидата CSP» + АПК «Валидата Клиент L»
# для работы Интеграционного шлюза НРД на Astra Linux.
#
# ВАЖНО: дистрибутив Валидаты не выложен публично. Получается по запросу:
# - НРД: soed@nsd.ru
# - МБ: pki@moex.com
# В письме указать: «Запрос дистрибутива СКЗИ Валидата CSP для Linux +
# временной лицензии для подключения к ЭДО НРД в рамках MOEX МОСТ M2M.»
#
# Скрипт ожидает что архив с дистрибутивом уже скачан и лежит:
# dist/validata/<любые>.deb
# или передан как первый аргумент.
#
# Запуск:
# sudo bash deploy/astra/install-validata.sh
# sudo bash deploy/astra/install-validata.sh /path/to/validata-csp.deb
set -euo pipefail
log() { echo -e "\033[1;34m[validata-install]\033[0m $*"; }
warn() { echo -e "\033[1;33m[validata-install WARN]\033[0m $*" >&2; }
fail() { echo -e "\033[1;31m[validata-install FAIL]\033[0m $*" >&2; exit 1; }
[ "$EUID" -eq 0 ] || fail "Запускать от root"
SEARCH_PATH="${1:-./dist/validata}"
if [ -f "$SEARCH_PATH" ] && [ "${SEARCH_PATH##*.}" = "deb" ]; then
# Передан конкретный файл
DEBS=( "$SEARCH_PATH" )
elif [ -d "$SEARCH_PATH" ]; then
mapfile -t DEBS < <(find "$SEARCH_PATH" -maxdepth 2 -name '*.deb' 2>/dev/null | sort)
else
fail "Не найден дистрибутив Валидаты. Положи .deb пакеты в dist/validata/ или передай путь аргументом.
Если у тебя ещё нет дистрибутива — запроси у НРД:
Email: soed@nsd.ru (или pki@moex.com)
Тема: Запрос дистрибутива Валидата CSP для Linux
Текст: Просим предоставить дистрибутив СКЗИ Валидата CSP v.6 для Linux
(Astra Linux SE 1.7) + временную лицензию для подключения к
ЭДО НРД через ПО Интеграционный шлюз в рамках сервиса
MOEX МОСТ M2M (см. инструкцию nsd.ru/workflow/system/programs/web-service/).
Реквизиты организации: <ИНН, ОГРН, контактное лицо>.
"
fi
if [ "${#DEBS[@]}" = 0 ]; then
fail "В каталоге $SEARCH_PATH не найдено ни одного .deb пакета"
fi
log "Найдено ${#DEBS[@]} пакетов Валидаты:"
for f in "${DEBS[@]}"; do
echo " $f"
done
log "Установка через dpkg..."
for f in "${DEBS[@]}"; do
log " $f"
dpkg -i "$f" || {
warn " → пытаюсь починить зависимости"
apt-get install -f -y
dpkg -i "$f"
}
done
# Проверка
log "Проверка установки..."
VAL_FOUND=0
for path in /opt/Validata /opt/validata-csp /opt/Validata-CSP; do
[ -d "$path" ] && { log " ✓ Валидата в $path"; VAL_FOUND=1; }
done
if [ "$VAL_FOUND" = 0 ]; then
warn "Каталог Валидаты не нашёл — проверь dpkg -L <имя-пакета>"
fi
echo
echo "================================================================"
echo " Валидата установлена"
echo "================================================================"
echo " Следующие шаги:"
echo " 1. Запустить Справочник сертификатов АПК Валидата Клиент"
echo " (GUI приложение)"
echo " 2. Загрузить корневой сертификат УЦ Московской Биржи"
echo " (взять у УЦ МБ — ca.moex.com — для своей организации)"
echo " 3. Импортировать пользовательский сертификат с приватным ключом"
echo " 4. Меню Сервис → Экспортировать сертификаты в системное хранилище"
echo " 5. Установить ИШ: sudo bash deploy/astra/install-ish.sh"
echo "================================================================"
+384
View File
@@ -0,0 +1,384 @@
#!/bin/bash
# install.sh — установка Bridge-and-Join-s одной командой.
#
# ЦЕЛЕВАЯ АУДИТОРИЯ: оператор без знания Linux/Go. Просто запускает строку:
#
# curl -sSL https://git.zetit.ru/zuevav/Bridge-and-Join-s/raw/main/deploy/astra/install.sh | sudo bash
#
# и всё работает.
#
# Поддерживаемые ОС:
# - Astra Linux Special Edition 1.6 / 1.7 (платная, для прод)
# - Astra Linux Common Edition / 1.8 (бесплатная)
# - Debian 11 / 12
# - Ubuntu 22.04 / 24.04 (с предупреждением)
#
# Что устанавливается АВТОМАТИЧЕСКИ:
# 1. Системные зависимости (apt: curl, git, podman, postgresql-client)
# 2. Go 1.24+ (скачивается с go.dev)
# 3. PostgreSQL 16 в podman-контейнере + миграции
# 4. bj-server (компилируется из исходников, ставится в /opt/bj/)
# 5. Дистрибутив ИШ НРД (скачивается с сайта НРД, ~120 МБ)
# 6. Сам ИШ устанавливается через dpkg -i (но не запускается без Валидаты)
# 7. systemd unit + автозапуск
#
# Что НЕ автоматизируется (только пользователь):
# - СКЗИ Валидата CSP — выдаётся НРД по запросу (soed@nsd.ru)
# - Сертификат подписи УЦ Московской Биржи (ca.moex.com)
# - Регистрация в TEST3 (заявка через nsd.ru)
set -euo pipefail
# ---- параметры ----
REPO_URL="${REPO_URL:-https://git.zetit.ru/zuevav/Bridge-and-Join-s.git}"
BRANCH="${BRANCH:-main}"
BIND_ADDR="${BIND_ADDR:-:8080}"
ISH_DEB_URL="${ISH_DEB_URL:-https://old.nsd.ru/upload/docs/edo/po/igate_100.0-765_amd64.deb}"
SKIP_ISH=0
NON_INTERACTIVE="${NON_INTERACTIVE:-0}"
for arg in "$@"; do
case "$arg" in
--skip-ish) SKIP_ISH=1 ;;
--bind=*) BIND_ADDR="${arg#*=}" ;;
--branch=*) BRANCH="${arg#*=}" ;;
--yes|-y) NON_INTERACTIVE=1 ;;
esac
done
# ---- утилиты вывода ----
NS=$(date +%s)
step() { local n=$(( $(date +%s) - NS )); printf "\033[1;36m[%4ds]\033[0m \033[1;34m▶\033[0m %s\n" "$n" "$*"; }
ok() { printf " \033[1;32m✓\033[0m %s\n" "$*"; }
warn() { printf " \033[1;33m⚠\033[0m %s\n" "$*"; }
fail() { printf " \033[1;31m✗\033[0m %s\n" "$*" >&2; exit 1; }
ask() {
[ "$NON_INTERACTIVE" = "1" ] && return 0
printf " \033[1;35m?\033[0m %s [y/N]: " "$*"
read -r REPLY < /dev/tty || REPLY=n
[ "$REPLY" = "y" ] || [ "$REPLY" = "Y" ]
}
# ---- баннер ----
clear 2>/dev/null || true
cat <<'BANNER'
╔══════════════════════════════════════════════════════════════════╗
║ ║
║ Bridge-and-Join-s — установка с нуля ║
║ сервис M2M-переводов с НКО АО НРД ║
║ ║
║ Установка займёт ~5-10 минут ║
║ Скачается ~150-250 МБ (Go + ИШ + миграции) ║
║ ║
╚══════════════════════════════════════════════════════════════════╝
BANNER
echo
[ "$EUID" -eq 0 ] || fail "Запускать от root: sudo bash $0"
# ============================================================
# ШАГ 1/9. Определение ОС
# ============================================================
step "1/9: определение операционной системы"
OS_KIND=""
OS_NAME="неизвестно"
if [ -r /etc/astra_version ]; then
OS_NAME="Astra Linux $(cat /etc/astra_version)"
OS_KIND="astra"
elif [ -r /etc/os-release ]; then
. /etc/os-release
OS_NAME="$PRETTY_NAME"
case "${ID:-}" in
astra) OS_KIND="astra" ;;
debian) OS_KIND="debian" ;;
ubuntu) OS_KIND="ubuntu" ;;
*)
case "${ID_LIKE:-}" in
*debian*) OS_KIND="debian-like" ;;
esac
;;
esac
fi
ok "Обнаружено: $OS_NAME"
case "$OS_KIND" in
astra)
ok "Astra Linux — полностью поддерживается, ИШ заработает официально"
;;
debian|"debian-like")
warn "Debian-based — bj-server установится, ИШ скорее всего тоже"
warn "(но официально НРД его не поддерживает на Debian; для прод-инфры лучше Astra Linux SE)"
;;
ubuntu)
warn "Ubuntu — bj-server установится, но ИШ может потребовать допилов"
ask "Продолжить?" || exit 1
;;
*)
fail "Неподдерживаемая ОС. Поддерживаются: Astra Linux (SE/CE), Debian, Ubuntu"
;;
esac
# ============================================================
# ШАГ 2/9. Системные пакеты
# ============================================================
step "2/9: установка системных пакетов через apt"
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq >/dev/null
apt-get install -y -qq \
ca-certificates curl wget git tar gzip \
podman postgresql-client \
>/dev/null 2>&1
# podman-compose доступен либо как apt-пакет, либо как pip — пробуем оба
if ! command -v podman-compose >/dev/null 2>&1; then
apt-get install -y -qq podman-compose >/dev/null 2>&1 || \
apt-get install -y -qq python3-pip >/dev/null 2>&1 && pip3 install --quiet podman-compose 2>/dev/null || true
fi
command -v podman >/dev/null && ok "podman: $(podman --version | awk '{print $3}')" || fail "podman не установился"
command -v git >/dev/null && ok "git: $(git --version | awk '{print $3}')" || fail "git не установился"
# ============================================================
# ШАГ 3/9. Go 1.24+
# ============================================================
step "3/9: Go 1.24+"
need_go=1
if command -v go >/dev/null 2>&1; then
GO_HAVE=$(go version | awk '{print $3}' | sed 's/go//')
if printf '%s\n%s' "1.24" "$GO_HAVE" | sort -V | head -1 | grep -q '^1.24$'; then
ok "Go $GO_HAVE — подходит"
need_go=0
else
warn "Go $GO_HAVE — слишком старый, обновляю"
fi
fi
if [ "$need_go" = 1 ]; then
GO_VER="1.24.0"
ok "качаю Go $GO_VER с go.dev (~70 МБ)..."
curl -sSL "https://go.dev/dl/go${GO_VER}.linux-amd64.tar.gz" -o /tmp/go.tar.gz \
|| fail "не получилось скачать Go (нужен интернет)"
rm -rf /usr/local/go
tar -C /usr/local -xzf /tmp/go.tar.gz
ln -sf /usr/local/go/bin/go /usr/local/bin/go
ln -sf /usr/local/go/bin/gofmt /usr/local/bin/gofmt
rm -f /tmp/go.tar.gz
ok "Go $GO_VER установлен в /usr/local/go"
fi
# ============================================================
# ШАГ 4/9. Пользователь bj и каталоги
# ============================================================
step "4/9: системный пользователь bj и каталоги"
if ! id bj >/dev/null 2>&1; then
useradd --system --create-home --home-dir /var/lib/bj --shell /bin/bash bj
ok "создан пользователь bj"
else
ok "пользователь bj уже существует"
fi
install -d -o bj -g bj -m 0755 /opt/bj /var/lib/bj /var/log/bj
install -d -o bj -g bj -m 0700 /var/lib/bj/.bj
ok "каталоги: /opt/bj /var/lib/bj /var/log/bj"
# ============================================================
# ШАГ 5/9. Клон репо и сборка bj-server
# ============================================================
step "5/9: клон репозитория и сборка bj-server"
SRC=/opt/bj/src
if [ -d "$SRC/.git" ]; then
sudo -u bj -H git -C "$SRC" fetch --quiet origin
sudo -u bj -H git -C "$SRC" reset --hard --quiet "origin/$BRANCH"
ok "репо обновлено до $BRANCH"
else
sudo -u bj -H git clone --quiet --branch "$BRANCH" "$REPO_URL" "$SRC" \
|| fail "git clone failed"
ok "репо склонирован"
fi
chown -R bj:bj "$SRC"
ok "компиляция bj-server..."
sudo -u bj -H bash -c "cd $SRC && /usr/local/bin/go build -o /opt/bj/bj-server ./cmd/bj-server" \
|| fail "go build failed"
chown bj:bj /opt/bj/bj-server
chmod 0755 /opt/bj/bj-server
ok "бинарник: /opt/bj/bj-server ($(du -h /opt/bj/bj-server | awk '{print $1}'))"
# ============================================================
# ШАГ 6/9. PostgreSQL в podman + миграции
# ============================================================
step "6/9: PostgreSQL в podman + миграции БД"
cd "$SRC"
if ! podman ps --format '{{.Names}}' 2>/dev/null | grep -qx bj-postgres; then
sudo -u bj -H podman-compose -f deploy/docker-compose/docker-compose.yml up -d postgres \
2>/dev/null || {
warn "podman-compose не сработал, пробую podman run напрямую"
sudo -u bj -H podman run -d --name bj-postgres \
-e POSTGRES_USER=bj -e POSTGRES_PASSWORD=bj_dev -e POSTGRES_DB=bj \
-p 127.0.0.1:5432:5432 \
docker.io/library/postgres:16-alpine
}
sleep 5
fi
# Ждём pg_isready
for i in 1 2 3 4 5 6 7 8 9 10; do
if sudo -u bj -H podman exec bj-postgres pg_isready -U bj -d bj >/dev/null 2>&1; then
ok "PostgreSQL готов"
break
fi
sleep 2
done
# Накат миграций
MIG_COUNT=0
for mig in migrations/fansy-store/*.sql migrations/m2m-core/*.sql; do
if [ -f "$mig" ]; then
sudo -u bj -H podman exec -i bj-postgres psql -U bj -d bj -v ON_ERROR_STOP=0 < "$mig" >/dev/null 2>&1 && \
MIG_COUNT=$((MIG_COUNT+1))
fi
done
ok "миграций накачено: $MIG_COUNT"
# Сохраняем DSN
cat > /var/lib/bj/.bj/setup.json <<EOF
{
"postgres": {"dsn": "postgres://bj:bj_dev@127.0.0.1:5432/bj?sslmode=disable"},
"crypto": {"provider": "stub", "socket_path": "/run/bj/crypto.sock"},
"nsd": {},
"lk": {},
"ca_certs": {},
"news": {},
"updated_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
}
EOF
chown bj:bj /var/lib/bj/.bj/setup.json
chmod 0600 /var/lib/bj/.bj/setup.json
ok "DSN сохранён в /var/lib/bj/.bj/setup.json"
# ============================================================
# ШАГ 7/9. systemd unit
# ============================================================
step "7/9: systemd unit для bj-server"
cat > /etc/systemd/system/bj-server.service <<EOF
[Unit]
Description=Bridge-and-Join-s — единый сервис M2M-переводов
Documentation=$REPO_URL
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=bj
Group=bj
WorkingDirectory=$SRC
ExecStart=/opt/bj/bj-server
Restart=on-failure
RestartSec=5
Environment=BJ_HTTP_ADDR=$BIND_ADDR
Environment=BJ_SETUP_PATH=/var/lib/bj/.bj/setup.json
Environment=BJ_M2M_SENDER=MC0079200000
Environment=BJ_M2M_RECEIVER=MC0010300000
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/bj /var/log/bj
PrivateTmp=true
StandardOutput=append:/var/log/bj/bj-server.log
StandardError=append:/var/log/bj/bj-server.err
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable bj-server >/dev/null 2>&1
systemctl restart bj-server
sleep 2
if systemctl is-active --quiet bj-server; then
ok "bj-server.service active"
else
warn "bj-server не стартанул, см. journalctl -u bj-server -n 30"
fi
# ============================================================
# ШАГ 8/9. ИШ НРД — скачивание и установка
# ============================================================
if [ "$SKIP_ISH" = "1" ]; then
step "8/9: ИШ НРД — пропущено (--skip-ish)"
else
step "8/9: Интеграционный шлюз НРД (ИШ)"
ISH_LOCAL="$SRC/dist/ish/igate_100.0-765_amd64.deb"
if [ -f "$ISH_LOCAL" ]; then
ok "дистрибутив ИШ уже в репо: $ISH_LOCAL"
else
ok "качаю дистрибутив ИШ с НРД (~120 МБ)..."
mkdir -p "$(dirname "$ISH_LOCAL")"
if curl -sSL -A "Mozilla/5.0" "$ISH_DEB_URL" -o "$ISH_LOCAL" --max-time 600; then
ok "скачан: $(du -h "$ISH_LOCAL" | awk '{print $1}')"
else
warn "не получилось скачать ИШ автоматически"
warn "скачайте вручную: $ISH_DEB_URL"
warn "и положите в $ISH_LOCAL, потом перезапустите этот скрипт"
ISH_LOCAL=""
fi
fi
if [ -n "$ISH_LOCAL" ] && [ -f "$ISH_LOCAL" ]; then
ok "установка ИШ через dpkg..."
if dpkg -i "$ISH_LOCAL" >/dev/null 2>&1; then
ok "ИШ установлен"
else
# Часто dpkg падает на зависимостях — пробуем apt-get install -f
apt-get install -f -y >/dev/null 2>&1
if dpkg -i "$ISH_LOCAL" >/dev/null 2>&1; then
ok "ИШ установлен (после починки зависимостей)"
else
warn "ИШ не встал — возможно нет Валидаты или системных пакетов"
warn "это нормально на текущем этапе — продолжаем"
fi
fi
fi
fi
# ============================================================
# ШАГ 9/9. Финальная проверка
# ============================================================
step "9/9: проверка готовности"
sleep 1
CODE=$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1${BIND_ADDR}/admin/" 2>/dev/null || echo "—")
[ "$CODE" = "200" ] && ok "веб-админка отвечает: HTTP 200" || warn "веб-админка пока не отвечает (HTTP $CODE) — проверь логи"
IP=$(hostname -I | awk '{print $1}')
echo
echo "╔══════════════════════════════════════════════════════════════════╗"
echo "║ УСТАНОВКА BJ-SERVER ЗАВЕРШЕНА ║"
echo "╚══════════════════════════════════════════════════════════════════╝"
echo
echo " Веб-админка: http://$IP${BIND_ADDR}/admin/"
echo " Мастер настройки: http://$IP${BIND_ADDR}/admin/wizard"
echo " Архитектура: http://$IP${BIND_ADDR}/admin/help/architecture"
echo " Новости: http://$IP${BIND_ADDR}/admin/news"
echo
echo " Логи: tail -f /var/log/bj/bj-server.log"
echo " Сервис: systemctl status bj-server"
echo
echo " ──── ЧТО ОСТАЛОСЬ СДЕЛАТЬ (НЕ АВТОМАТИЧЕСКИ) ───────────────"
echo
echo " 1. Запросить СКЗИ Валидата CSP у НРД:"
echo " Email: soed@nsd.ru"
echo " Текст: «Запрос дистрибутива Валидата CSP для Linux + временной"
echo " лицензии для подключения к ЭДО НРД в рамках MOEX МОСТ M2M.»"
echo
echo " 2. Получить сертификат подписи в УЦ Московской Биржи:"
echo " https://ca.moex.com/"
echo
echo " 3. Подать заявку на тестирование в TEST3:"
echo " https://www.nsd.ru/workflow/zayavka-na-testirovanie/"
echo
echo " 4. Когда придёт Валидата — поставить:"
echo " sudo bash $SRC/deploy/astra/install-validata.sh /path/to/validata-*.deb"
echo
echo " 5. Когда заработает ИШ — указать его URL в /admin/setup → «ИШ НРД»"
echo
echo " ──── ПРОВЕРКА СОСТОЯНИЯ ВСЕГО ──────────────────────────────"
echo " sudo bash $SRC/deploy/astra/healthcheck.sh"
echo
+62
View File
@@ -0,0 +1,62 @@
# Дистрибутив Интеграционного шлюза НРД (ИШ)
**Скачано с сайта НРД** (`https://www.nsd.ru/workflow/system/programs/web-service/`) 14.05.2026.
Через git не коммитим — файлы большие, ставятся отдельно.
## Файлы
| Файл | Размер | Описание |
|---|---:|---|
| `igate_100.0-765_amd64.deb` | 117 МБ | Дистрибутив ИШ для **Astra-Linux** (.deb пакет) |
| `igate_95.0-716_amd64.SGN` | 491 байт | Электронная подпись к дистрибутиву ИШ |
## Где скачать заново
- ИШ Linux: `https://old.nsd.ru/upload/docs/edo/po/igate_100.0-765_amd64.deb`
- ИШ Windows (рус): `https://old.nsd.ru/upload/docs/edo/po/igate-ru-100.0.0.764.zip`
- ИШ Windows (eng): `https://old.nsd.ru/upload/docs/edo/po/igate-en-100.0.0.764.zip`
- Все версии: `https://www.nsd.ru/workflow/system/programs/web-service/`
## Что ещё нужно (НЕ в этой папке)
### 1. СКЗИ «Валидата CSP» + АПК «Валидата Клиент L»
**Не выложено публично** — даётся НРД по запросу:
- Email НРД: `soed@nsd.ru`
- Email Московской Биржи: `pki@moex.com`
В письме указать: «Запрос дистрибутива СКЗИ Валидата CSP для Linux + временной лицензии для подключения к ЭДО НРД в рамках сервиса MOEX МОСТ M2M».
### 2. Сертификат подписи
Только от **УЦ Московской Биржи** (`https://ca.moex.com/`). Получает организация-депонент.
### 3. PostgreSQL
Если используется REST API ИШ — **обязательно** PostgreSQL (SQLite не подходит для API).
У нас PostgreSQL 16 уже работает в podman-контейнере → готово.
## Поддерживаемые ОС (из руководства по установке)
- **Astra Linux Special Edition x64** редакций 1.6, 1.7, исполнение 1 (РУСБ.10015-01/16)
- **Windows 10 / Server 2016/2019**
**РЕД ОС в списке не упомянута.** Варианты для нашей инфраструктуры:
1. Поднять отдельную Astra Linux ВМ для ИШ (рекомендуется)
2. Попробовать `dpkg -i` на РЕД ОС с `alien` (рискованно)
3. Использовать Debian/Ubuntu ВМ (близко к Astra, возможно сработает)
4. Контейнер с базовым образом `astralinux/astra-linux-edu:1.7.5` (если такой есть)
5. Запросить у НРД RPM-версию
## Контакты НРД
- Email по СЭД и дистрибутивам: `soed@nsd.ru`
- Email по форматам M2M: `M2MOST@nsd.ru`
- Сайт ИШ: `https://www.nsd.ru/workflow/system/programs/web-service/`
## Документация
Все PDF лежат в `../../DOC/`:
- `ruk_install_ish_2025_11_10.pdf` — Руководство по установке ИШ (от 10.11.2025)
- `ruk_pol_ish.pdf` — Руководство пользователя ИШ
- `QA_ish.pdf` — Часто задаваемые вопросы
- `test-case_ish.pdf` — Тест-кейсы для проверки работоспособности ИШ
- `instr_int_sh_01072025.pdf` — Инструкция по созданию заявки на тестирование
- `web_service_nrd_standard_soap_rest.pdf` — Технические рекомендации Web-сервиса ONYX
+9 -3
View File
@@ -20,8 +20,8 @@ var templatesFS embed.FS
// {{define "content"}} в разных файлах. // {{define "content"}} в разных файлах.
type admin struct { type admin struct {
home, claims, claim, status, setup *template.Template home, claims, claim, status, setup *template.Template
help, helpDatabase, helpLK, helpCryptoPro, helpSystems, helpRobot *template.Template help, helpDatabase, helpLK, helpCryptoPro, helpSystems, helpRobot, helpArchitecture *template.Template
wizard, news *template.Template wizard, news *template.Template
} }
// templateFuncs — функции, доступные внутри шаблонов. Главная задача — // templateFuncs — функции, доступные внутри шаблонов. Главная задача —
@@ -135,10 +135,14 @@ func newAdmin() (*admin, error) {
if err != nil { if err != nil {
return nil, fmt.Errorf("parse admin_help_robot: %w", err) return nil, fmt.Errorf("parse admin_help_robot: %w", err)
} }
helpArch, err := parse("admin_help_architecture.html")
if err != nil {
return nil, fmt.Errorf("parse admin_help_architecture: %w", err)
}
return &admin{ return &admin{
home: home, claims: claims, claim: claim, status: status, setup: setup, home: home, claims: claims, claim: claim, status: status, setup: setup,
help: help, helpDatabase: helpDB, helpLK: helpLK, helpCryptoPro: helpCP, helpSystems: helpSys, help: help, helpDatabase: helpDB, helpLK: helpLK, helpCryptoPro: helpCP, helpSystems: helpSys,
helpRobot: helpRobot, helpRobot: helpRobot, helpArchitecture: helpArch,
wizard: wizard, news: news, wizard: wizard, news: news,
}, nil }, nil
} }
@@ -224,6 +228,8 @@ func RegisterAdmin(mux *http.ServeMux, svc *Service, rc *RuntimeConfig, getOpts
render(w, a.helpSystems, nowPage("Внешние системы", "help")) render(w, a.helpSystems, nowPage("Внешние системы", "help"))
case p == "help/robot": case p == "help/robot":
render(w, a.helpRobot, nowPage("Тестирование с роботом", "help")) render(w, a.helpRobot, nowPage("Тестирование с роботом", "help"))
case p == "help/architecture":
render(w, a.helpArchitecture, nowPage("Архитектура обмена", "help"))
default: default:
http.NotFound(w, r) http.NotFound(w, r)
} }
@@ -35,5 +35,11 @@
<p class="muted">Робот НРД на TEST3 (код <code>MC0012500000</code>), 4 тестовых сценария (отказ / принять все / частично / встречный перевод), управление через DocumentSeries и DocumentNumber, тестовые наборы депозитариев и кодов ошибок.</p> <p class="muted">Робот НРД на TEST3 (код <code>MC0012500000</code>), 4 тестовых сценария (отказ / принять все / частично / встречный перевод), управление через DocumentSeries и DocumentNumber, тестовые наборы депозитариев и кодов ошибок.</p>
</div> </div>
</a> </a>
<a href="/admin/help/architecture" style="text-decoration:none">
<div class="card" style="height:100%">
<h2 style="color:var(--accent)">Архитектура обмена с НРД →</h2>
<p class="muted">Полная схема: bj-server → ИШ (на Astra Linux ВМ) → ONYX (НРД) → робот-автотест. Кто на чьей стороне, какие СКЗИ, какие сертификаты, FAQ. Куда воткнуть Валидату, куда КриптоПро, где сертификаты УЦ МБ.</p>
</div>
</a>
</div> </div>
{{end}} {{end}}
@@ -0,0 +1,139 @@
{{define "content"}}
<p style="margin-bottom:16px"><a href="/admin/help">← все инструкции</a></p>
<div class="card">
<h2>Архитектура: как устроен обмен с НРД</h2>
<p class="muted">Документ-источник: <code>DOC/ruk_install_ish_2025_11_10.pdf</code> (Руководство по установке ИШ от 10.11.2025), <code>DOC/instr-ish-rest-api.pdf</code> (REST API ИШ).</p>
</div>
<div class="card">
<h2>Схема обмена (полная)</h2>
<pre style="font-size:12px;line-height:1.4;background:var(--bg);padding:16px;border-radius:6px;overflow:auto">
┌─────────────────────────────────────────────────────────────────┐
│ НАША СТОРОНА │
│ │
│ Linux ВМ (РЕД ОС 8) Astra Linux ВМ │
│ ────────────────── ────────────────── │
│ ┌──────────────────┐ REST API ┌──────────────────┐ │
│ │ bj-server │ ────POST/GET──> │ ИШ (igate) │ │
│ │ (наше ПО) │ <───────────── │ (получаем у НРД)│ │
│ │ │ │ │ │
│ │ • стейт-машина │ │ Делает САМ: │ │
│ │ • PostgreSQL │ │ • подпись │ │
│ │ • админка :8080 │ │ • упаковку ЭДО │ │
│ │ • lk-emulator │ │ • проверку │ │
│ │ │ │ подписей НРД │ │
│ └──────────────────┘ │ • БД PostgreSQL │ │
│ │ (history) │ │
│ └──────┬───────────┘ │
│ │ │
│ КриптоПро CSP — для нашей │ Валидата CSP │
│ admin-стороны (PKCS#11) │ + АПК Валидата │
│ │ Клиент L │
└──────────────────────────────────────────────┼─────────────────┘
SOAP/REST/HTTPS │ Web-сервис ONYX
┌────────────────────────────┐
│ СТОРОНА НРД │
│ │
│ GUEST: gost-gt.nsd.ru │
│ TEST3: gost.nsd.ru │
│ PROM: edog.nsd.ru │
│ │
│ /onyxgs/WslService │
│ /onyxt3/WslService │
│ /onyxpr/WslService │
│ │
│ ↓ внутрь НРД │
│ • робот-автотест │
│ MC0012500000 │
│ • реальные депозитарии │
│ • биржевые системы │
└────────────────────────────┘
</pre>
</div>
<div class="card">
<h2>Компоненты — кто на чьей стороне</h2>
<table>
<thead><tr><th>Компонент</th><th>Сторона</th><th>ОС</th><th>СКЗИ</th><th>Назначение</th></tr></thead>
<tbody>
<tr>
<td><strong>bj-server</strong></td>
<td>наша</td>
<td>РЕД ОС 8 / Linux</td>
<td>КриптоПро CSP (PKCS#11) — для админ-части</td>
<td>Стейт-машина, журнал в БД, веб-админка, lk-emulator</td>
</tr>
<tr>
<td><strong>ИШ (igate)</strong></td>
<td>наша <span class="muted">(но дистрибутив даёт НРД)</span></td>
<td>Astra Linux SE 1.6/1.7 <em>или</em> Windows 10/Server</td>
<td>Валидата CSP + АПК Валидата Клиент L</td>
<td>Подписывает наш XML сертификатом УЦ МБ, упаковывает в пакет ЭДО, отправляет в НРД</td>
</tr>
<tr>
<td><strong>ONYX (WSL)</strong></td>
<td>НРД</td>
<td></td>
<td></td>
<td>Web-сервис НРД — принимает пакеты от ИШ всех клиентов</td>
</tr>
<tr>
<td><strong>Робот-автотест</strong></td>
<td>НРД</td>
<td></td>
<td></td>
<td>Контрагент-эмулятор внутри НРД. Адресуется кодом <code>MC0012500000</code> в TEST3</td>
</tr>
</tbody>
</table>
</div>
<div class="card">
<h2>Часто задаваемые вопросы</h2>
<h3>Q: ИШ — это сервер НРД, к которому мы подключаемся?</h3>
<p>Нет. <strong>ИШ — это наша программа, поставленная у нас.</strong> НРД даёт дистрибутив (<code>igate_100.0-765_amd64.deb</code>, 117 МБ), но ставим у себя. ИШ — это «персональный почтовый клиент к НРД» с подписью.</p>
<h3>Q: ИШ можно поставить на ту же ВМ, что и bj-server?</h3>
<p>Технически да (если та ВМ — Astra Linux). Но у нас bj-server на РЕД ОС, а ИШ требует <strong>Astra Linux</strong> (RPM-версии нет). Поэтому нужно либо: (а) отдельная Astra Linux ВМ, (б) запуск ИШ в Docker-контейнере с Astra-образом, (в) перевод всей инфры на Astra Linux.</p>
<h3>Q: Мы перекладываем файлы между bj-server и ИШ?</h3>
<p>Нет. Мы используем <strong>REST API</strong> ИШ (раздел 2.5 инструкции). bj-server делает HTTP-запросы: <code>POST /api/package/{channel}/file</code> с ZIP в теле. Никаких разделяемых папок. (Альтернативный режим «обменные папки» в ИШ есть — мы его не используем.)</p>
<h3>Q: Почему ИШ требует Валидата CSP, а мы поставили КриптоПро?</h3>
<p>ИШ — отечественная разработка НРД, исторически работает с Валидатой (продукт ООО «Валидата», <code>x509.ru</code>). КриптоПро CSP на нашей ВМ останется — он используется для админ-части bj-server (подпись действий оператора через Рутокен). Валидату надо поставить <strong>на Astra Linux ВМ рядом с ИШ</strong>, не вместо КриптоПро.</p>
<h3>Q: Где брать Валидату?</h3>
<p>Не публично. По запросу: email <code>soed@nsd.ru</code> (НРД) или <code>pki@moex.com</code> (МБ). Временная лицензия выдаётся бесплатно для подключения к ЭДО НРД.</p>
<h3>Q: Какой сертификат нужен?</h3>
<p>Только от <strong>УЦ Московской Биржи</strong> (<code>ca.moex.com</code>). Сертификаты других УЦ ИШ не примет. УЦ МБ выпускает сертификаты только для организаций, подключённых к ЭДО НРД (по договору).</p>
<h3>Q: Что делать, чтобы протестировать на роботе НРД на TEST3?</h3>
<ol>
<li>Получить сертификат УЦ МБ для нашей организации.</li>
<li>Подать <a href="https://www.nsd.ru/workflow/zayavka-na-testirovanie/" target="_blank">заявку на тестирование</a> в НРД (инструкция в <code>DOC/instr_int_sh_01072025.pdf</code>).</li>
<li>Получить от НРД код депонента-тестера и доступ к TEST3.</li>
<li>Поднять Astra Linux ВМ, поставить ИШ + Валидату, импортировать сертификат.</li>
<li>В нашем <a href="/admin/setup">/admin/setup</a> → «Интеграционный шлюз НРД» указать URL ИШ (например <code>http://10.10.10.23:8080</code>) и имя канала из ИШ.</li>
<li>Отправить тестовую заявку с <code>ReceiverCode = MC0012500000</code> и <code>DocumentSeries = 2001</code> — робот ответит «Принять все бумаги».</li>
</ol>
<h3>Q: Сколько времени нужно от старта подключения до прогона на TEST3?</h3>
<p>Оптимистично — <strong>1-2 недели</strong> (если все ответы НРД быстрые и УЦ МБ не задерживает). Реалистично — <strong>3-4 недели</strong>. На нашей стороне всё уже готово; задержка только во внешних шагах.</p>
</div>
<div class="card">
<h2>Что у нас уже готово</h2>
<ul>
<li><strong>REST-клиент ИШ</strong> в <code>internal/nsdadapter/igw/</code> — все 4 endpoint'а по спецификации, упаковщик/распаковщик ZIP, 10 тестов PASS</li>
<li><strong>Робот-эмулятор</strong> в <code>internal/nsdadapter/mock/</code> — позволяет проверить нашу логику до получения реального ИШ</li>
<li><strong>Конфигурация в админке</strong> — поля <code>igw_base_url</code> и <code>channel</code> в /admin/setup; авто-определение профилей GUEST/TEST3/PROD</li>
<li><strong>Подбор URL контуров</strong> — при выборе профиля URL ONYX заполняется автоматически</li>
<li><strong>Полная документация ИШ</strong> в <code>DOC/</code> и дистрибутив в <code>dist/ish/</code></li>
</ul>
</div>
{{end}}
+223 -53
View File
@@ -1,6 +1,20 @@
// Package igw — REST-клиент Интеграционного шлюза (ИШ) НРД. // Package igw — REST-клиент Интеграционного шлюза (ИШ) НРД.
// Тело пакета передаётся base64 в JSON; ИШ сам подписывает и // Документ-источник: DOC/instr-ish-rest-api.pdf (НРД, 2026).
// упаковывает в ZIP-пакет ЭДО по правилам НРД. //
// ИШ — серверное ПО НРД, которое:
// - принимает от нас сырой XML/ZIP M2M-документа;
// - сам подписывает его сертификатом УЦ МБ (ключ настроен в ИШ);
// - формирует пакет ЭДО по Правилам ЭДО НРД;
// - отправляет в НРД через Web-сервис ONYX;
// - принимает входящие пакеты M2MTD/M2MER от НРД;
// - проверяет подпись НРД (поле signs.status = VALID/INVALID);
// - выдаёт всё это клиенту через REST API.
//
// REST-эндпоинты (все по 200/JSON):
// POST /api/package/{channel}/file — отправить ZIP, вернёт id
// GET /api/package/status/{id} — статус: NEW | SENT | ERROR
// GET /api/package?channel=&type=... — список входящих
// GET /api/package/{id} — тело пакета (ZIP в base64)
package igw package igw
import ( import (
@@ -13,6 +27,7 @@ import (
"io" "io"
"net/http" "net/http"
"net/url" "net/url"
"strconv"
"strings" "strings"
"time" "time"
) )
@@ -53,21 +68,42 @@ func NewClient(baseURL string, opts ...Option) *Client {
return c return c
} }
// SendPackage отправляет пакет в указанный канал ЭДО. Возвращает // --------------- POST /api/package/{channel}/file ---------------
// идентификатор пакета, присвоенный ИШ.
func (c *Client) SendPackage(ctx context.Context, channel, packageType string, body []byte) (string, error) { // sendBody — тело запроса на отправку пакета. По спецификации НРД
// (instr-ish-rest-api.pdf, раздел 2.5.1) поля Type/File.
type sendBody struct {
Type string `json:"Type"`
File string `json:"File"`
}
// sendResponse — ответ ИШ на отправку. Спецификация: 200 + JSON с ID.
// В новом документе НРД сам формат JSON не зафиксирован детально, поэтому
// принимаем три популярные формы: {id:..}, {package_id:..}, {ID:..}.
type sendResponse struct {
ID json.Number `json:"id,omitempty"`
PackageID json.Number `json:"package_id,omitempty"`
IDAlt json.Number `json:"ID,omitempty"`
}
// SendPackage отправляет ZIP-архив (M2MTransferRequest.xml + config.xml)
// в указанный канал ЭДО ИШ. Сигнатура совместима с предыдущей версией —
// packageType остался параметром для backward-compat, но в новом API
// он внутри ZIP'а (в config.xml/<package>), не в HTTP-теле.
//
// Возвращает идентификатор пакета (как строку — может быть числом или
// UUID, зависит от версии ИШ).
func (c *Client) SendPackage(ctx context.Context, channel, packageType string, zipBody []byte) (string, error) {
if channel == "" { if channel == "" {
return "", errors.New("igw: channel пустой") return "", errors.New("igw: channel пустой")
} }
if packageType == "" { _ = packageType // не используется в новом API, кладётся внутрь ZIP/config.xml
return "", errors.New("igw: packageType пустой") if len(zipBody) == 0 {
return "", errors.New("igw: zipBody пустой")
} }
payload := struct { payload := sendBody{
PackageType string `json:"package_type"` Type: "archive",
Body string `json:"body"` File: base64.StdEncoding.EncodeToString(zipBody),
}{
PackageType: packageType,
Body: base64.StdEncoding.EncodeToString(body),
} }
raw, err := json.Marshal(payload) raw, err := json.Marshal(payload)
if err != nil { if err != nil {
@@ -80,27 +116,36 @@ func (c *Client) SendPackage(ctx context.Context, channel, packageType string, b
} }
defer resp.Body.Close() defer resp.Body.Close()
var out struct { var out sendResponse
PackageID string `json:"package_id"`
}
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
return "", fmt.Errorf("igw: decode SendPackage response: %w", err) return "", fmt.Errorf("igw: decode SendPackage response: %w", err)
} }
if out.PackageID == "" { for _, v := range []json.Number{out.ID, out.PackageID, out.IDAlt} {
return "", errors.New("igw: пустой package_id в ответе ИШ") if s := string(v); s != "" {
return s, nil
}
} }
return out.PackageID, nil return "", errors.New("igw: пустой id в ответе ИШ")
} }
// Status — состояние пакета у ИШ. // --------------- GET /api/package/status/{id} ---------------
// Status — состояние отправленного пакета (раздел 2.5.2 инструкции).
// Status может быть NEW (новый), SENT (отправлен), ERROR (ошибка).
type Status struct { type Status struct {
PackageID string `json:"package_id"` ID string `json:"id"`
State string `json:"state"` Name string `json:"name"`
UpdatedAt time.Time `json:"updated_at"` Status string `json:"status"`
ErrorCode string `json:"error_code,omitempty"` Error string `json:"error,omitempty"`
ErrorText string `json:"error_text,omitempty"`
} }
// Status-константы для удобства.
const (
StatusNew = "NEW"
StatusSent = "SENT"
StatusError = "ERROR"
)
// GetStatus возвращает текущее состояние пакета по идентификатору. // GetStatus возвращает текущее состояние пакета по идентификатору.
func (c *Client) GetStatus(ctx context.Context, packageID string) (Status, error) { func (c *Client) GetStatus(ctx context.Context, packageID string) (Status, error) {
if packageID == "" { if packageID == "" {
@@ -112,41 +157,79 @@ func (c *Client) GetStatus(ctx context.Context, packageID string) (Status, error
return Status{}, err return Status{}, err
} }
defer resp.Body.Close() defer resp.Body.Close()
var s Status // Дублируем альтернативные имена полей (id|ID, status|state) — на
if err := json.NewDecoder(resp.Body).Decode(&s); err != nil { // случай различий между версиями ИШ.
var raw map[string]json.RawMessage
if err := json.NewDecoder(resp.Body).Decode(&raw); err != nil {
return Status{}, fmt.Errorf("igw: decode Status: %w", err) return Status{}, fmt.Errorf("igw: decode Status: %w", err)
} }
s := Status{}
pickStr(raw, []string{"id", "ID", "package_id"}, &s.ID)
pickStr(raw, []string{"name"}, &s.Name)
pickStr(raw, []string{"status", "state"}, &s.Status)
pickStr(raw, []string{"error", "error_text", "error_code"}, &s.Error)
return s, nil return s, nil
} }
// Package — описание входящего пакета. // --------------- GET /api/package?channel=&type=... ---------------
// ListFilter — параметры фильтрации входящих пакетов (раздел 2.6).
type ListFilter struct {
Channel string // обязательный — код канала ЭДО
Date time.Time // опц., YYYY-MM-DD
SinceID int // опц., скип до этого id
Count int // опц., лимит
Type string // опц., "M2MTD" | "M2MER" (без #)
ExcludeErrors bool // опц., исключать пакеты с ошибкой
}
// Sign — подпись пакета. ИШ сам проверяет и выдаёт результат: VALID/INVALID.
type Sign struct {
Serial string `json:"serial"`
Subject string `json:"subject"`
Description string `json:"description"`
Status string `json:"status"` // "VALID" | "INVALID"
}
// File — файл внутри пакета.
type File struct {
ID int `json:"id"`
Name string `json:"name"`
}
// Package — описание входящего пакета по списку (раздел 2.6).
type Package struct { type Package struct {
PackageID string `json:"package_id"` Channel string `json:"channel"`
PackageType string `json:"package_type"` ID int `json:"id"`
Channel string `json:"channel"` Name string `json:"name"`
ReceivedAt time.Time `json:"received_at"` Type string `json:"type"` // "M2MTD" | "M2MER"
Body string `json:"body,omitempty"` // base64 State string `json:"state"` // "RECEIVED" | "ERROR" | "DELETED"
Files []File `json:"files,omitempty"`
Signs []Sign `json:"signs,omitempty"`
} }
// DecodeBody возвращает декодированное содержимое пакета. // ListIncoming возвращает список входящих пакетов от НРД по фильтрам.
func (p Package) DecodeBody() ([]byte, error) { // Если filter.Type не задан — возвращает оба типа M2MTD + M2MER.
if p.Body == "" { func (c *Client) ListIncoming(ctx context.Context, filter ListFilter) ([]Package, error) {
return nil, nil if filter.Channel == "" {
return nil, errors.New("igw: ListFilter.Channel обязателен")
} }
return base64.StdEncoding.DecodeString(p.Body)
}
// ListIncoming возвращает список входящих пакетов по фильтрам.
func (c *Client) ListIncoming(ctx context.Context, channel string, since time.Time, packageType string) ([]Package, error) {
q := url.Values{} q := url.Values{}
if channel != "" { q.Set("channel", filter.Channel)
q.Set("channel", channel) if !filter.Date.IsZero() {
q.Set("date", filter.Date.UTC().Format("2006-01-02"))
} }
if !since.IsZero() { if filter.SinceID > 0 {
q.Set("date", since.UTC().Format(time.RFC3339)) q.Set("id", strconv.Itoa(filter.SinceID))
} }
if packageType != "" { if filter.Count > 0 {
q.Set("type", packageType) q.Set("count", strconv.Itoa(filter.Count))
}
if filter.Type != "" {
q.Set("type", filter.Type)
}
if filter.ExcludeErrors {
q.Set("excludeErrors", "true")
} }
path := "/api/package?" + q.Encode() path := "/api/package?" + q.Encode()
resp, err := c.doRetry(ctx, http.MethodGet, path, nil, "") resp, err := c.doRetry(ctx, http.MethodGet, path, nil, "")
@@ -154,15 +237,83 @@ func (c *Client) ListIncoming(ctx context.Context, channel string, since time.Ti
return nil, err return nil, err
} }
defer resp.Body.Close() defer resp.Body.Close()
var out struct {
// ИШ может вернуть либо массив, либо {items: [...]}. Поддержим оба.
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("igw: read ListIncoming: %w", err)
}
body = bytes.TrimSpace(body)
if len(body) == 0 {
return nil, nil
}
if body[0] == '[' {
var arr []Package
if err := json.Unmarshal(body, &arr); err != nil {
return nil, fmt.Errorf("igw: decode ListIncoming (array): %w", err)
}
return arr, nil
}
var wrap struct {
Items []Package `json:"items"` Items []Package `json:"items"`
} }
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { if err := json.Unmarshal(body, &wrap); err != nil {
return nil, fmt.Errorf("igw: decode ListIncoming: %w", err) return nil, fmt.Errorf("igw: decode ListIncoming (object): %w", err)
} }
return out.Items, nil return wrap.Items, nil
} }
// --------------- GET /api/package/{id} ---------------
// GetPackage возвращает содержимое пакета по ID — ZIP-архив с файлами
// документа и отсоединёнными подписями. По спецификации (раздел 2.6.2)
// ИШ отвечает 200 + body = base64-encoded ZIP. Метод декодирует base64
// и возвращает сырые ZIP-байты.
func (c *Client) GetPackage(ctx context.Context, packageID int) ([]byte, error) {
if packageID <= 0 {
return nil, errors.New("igw: packageID должен быть > 0")
}
path := "/api/package/" + strconv.Itoa(packageID)
resp, err := c.doRetry(ctx, http.MethodGet, path, nil, "")
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("igw: read GetPackage: %w", err)
}
// ИШ возвращает либо чистый ZIP (Content-Type: application/zip),
// либо JSON с base64. Проверим по сигнатуре ZIP (PK\x03\x04).
if len(body) >= 4 && body[0] == 'P' && body[1] == 'K' &&
body[2] == 0x03 && body[3] == 0x04 {
return body, nil
}
// Иначе пробуем base64 (с JSON-обёрткой или без).
stripped := bytes.TrimSpace(body)
if len(stripped) > 1 && stripped[0] == '"' && stripped[len(stripped)-1] == '"' {
stripped = stripped[1 : len(stripped)-1]
}
// JSON-объект {"file":"..."} или {"body":"..."}
if len(stripped) > 0 && stripped[0] == '{' {
var obj map[string]string
if err := json.Unmarshal(stripped, &obj); err == nil {
for _, k := range []string{"file", "File", "body", "Body"} {
if v, ok := obj[k]; ok {
return base64.StdEncoding.DecodeString(v)
}
}
}
}
decoded, err := base64.StdEncoding.DecodeString(string(stripped))
if err == nil && len(decoded) >= 4 && decoded[0] == 'P' && decoded[1] == 'K' {
return decoded, nil
}
return body, nil
}
// --------------- общая HTTP-логика ---------------
// doRetry выполняет HTTP-запрос с ретраями на сетевые ошибки и 5xx. // doRetry выполняет HTTP-запрос с ретраями на сетевые ошибки и 5xx.
func (c *Client) doRetry(ctx context.Context, method, path string, body io.Reader, contentType string) (*http.Response, error) { func (c *Client) doRetry(ctx context.Context, method, path string, body io.Reader, contentType string) (*http.Response, error) {
var lastErr error var lastErr error
@@ -182,7 +333,7 @@ func (c *Client) doRetry(ctx context.Context, method, path string, body io.Reade
if contentType != "" { if contentType != "" {
req.Header.Set("Content-Type", contentType) req.Header.Set("Content-Type", contentType)
} }
req.Header.Set("Accept", "application/json") req.Header.Set("Accept", "application/json, */*")
resp, err := c.httpClient.Do(req) resp, err := c.httpClient.Do(req)
switch { switch {
case err != nil: case err != nil:
@@ -207,3 +358,22 @@ func (c *Client) doRetry(ctx context.Context, method, path string, body io.Reade
} }
return nil, fmt.Errorf("igw: исчерпаны ретраи: %w", lastErr) return nil, fmt.Errorf("igw: исчерпаны ретраи: %w", lastErr)
} }
// pickStr заполняет dest первым непустым значением из raw по списку ключей.
func pickStr(raw map[string]json.RawMessage, keys []string, dest *string) {
for _, k := range keys {
if v, ok := raw[k]; ok {
var s string
if err := json.Unmarshal(v, &s); err == nil && s != "" {
*dest = s
return
}
// Может быть числом — превращаем в строку.
var n json.Number
if err := json.Unmarshal(v, &n); err == nil && string(n) != "" {
*dest = string(n)
return
}
}
}
}
+108 -35
View File
@@ -2,32 +2,53 @@ package igw_test
import ( import (
"context" "context"
"encoding/base64"
"encoding/json" "encoding/json"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"strings"
"testing" "testing"
"time" "time"
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/nsdadapter/igw" "git.zetit.ru/zuevav/Bridge-and-Join-s/internal/nsdadapter/igw"
) )
// TestSendPackageHappyPath — отправка ZIP, ИШ возвращает id.
// Сценарий по DOC/instr-ish-rest-api.pdf раздел 2.5.1.
func TestSendPackageHappyPath(t *testing.T) { func TestSendPackageHappyPath(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/api/package/TEST3/file" { if r.URL.Path != "/api/package/CH1/file" {
t.Errorf("неожиданный путь %q", r.URL.Path) t.Errorf("неожиданный путь %q", r.URL.Path)
} }
// Проверим что тело — это {Type: "archive", File: base64}.
var body struct {
Type string `json:"Type"`
File string `json:"File"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
t.Fatalf("decode body: %v", err)
}
if body.Type != "archive" {
t.Errorf("Type = %q, ожидалось archive", body.Type)
}
if body.File == "" {
t.Errorf("File пустой")
}
if _, err := base64.StdEncoding.DecodeString(body.File); err != nil {
t.Errorf("File не base64: %v", err)
}
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(map[string]string{"package_id": "pkg-123"}) _ = json.NewEncoder(w).Encode(map[string]any{"id": 123})
})) }))
defer srv.Close() defer srv.Close()
c := igw.NewClient(srv.URL, igw.WithRetry(0, time.Millisecond)) c := igw.NewClient(srv.URL, igw.WithRetry(0, time.Millisecond))
id, err := c.SendPackage(context.Background(), "TEST3", "#M2MTR", []byte("<xml/>")) id, err := c.SendPackage(context.Background(), "CH1", "#M2MTR", []byte("PK\x03\x04zipbody"))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if id != "pkg-123" { if id != "123" {
t.Errorf("package_id = %q, ожидалось %q", id, "pkg-123") t.Errorf("id = %q, ожидалось 123", id)
} }
} }
@@ -40,17 +61,17 @@ func TestSendPackageRetryOn500(t *testing.T) {
return return
} }
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(map[string]string{"package_id": "pkg-retry"}) _ = json.NewEncoder(w).Encode(map[string]any{"id": 999})
})) }))
defer srv.Close() defer srv.Close()
c := igw.NewClient(srv.URL, igw.WithRetry(3, time.Millisecond)) c := igw.NewClient(srv.URL, igw.WithRetry(3, time.Millisecond))
id, err := c.SendPackage(context.Background(), "TEST3", "#M2MTR", []byte("x")) id, err := c.SendPackage(context.Background(), "CH1", "#M2MTR", []byte("x"))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if id != "pkg-retry" { if id != "999" {
t.Errorf("ожидалось pkg-retry, получено %q", id) t.Errorf("id = %q, ожидалось 999", id)
} }
if calls < 2 { if calls < 2 {
t.Errorf("ожидалось хотя бы 2 попытки, получено %d", calls) t.Errorf("ожидалось хотя бы 2 попытки, получено %d", calls)
@@ -67,7 +88,7 @@ func TestSendPackage4xxNoRetry(t *testing.T) {
defer srv.Close() defer srv.Close()
c := igw.NewClient(srv.URL, igw.WithRetry(3, time.Millisecond)) c := igw.NewClient(srv.URL, igw.WithRetry(3, time.Millisecond))
_, err := c.SendPackage(context.Background(), "TEST3", "#M2MTR", []byte("x")) _, err := c.SendPackage(context.Background(), "CH1", "#M2MTR", []byte("x"))
if err == nil { if err == nil {
t.Fatal("ожидалась ошибка на 400") t.Fatal("ожидалась ошибка на 400")
} }
@@ -76,60 +97,112 @@ func TestSendPackage4xxNoRetry(t *testing.T) {
} }
} }
// TestGetStatus — формат ответа по разделу 2.5.2 инструкции:
// {id: 123, name: "#M2MTR...zip", status: SENT|NEW|ERROR, error: "..."}.
func TestGetStatus(t *testing.T) { func TestGetStatus(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/api/package/status/pkg-1" { if r.URL.Path != "/api/package/status/123" {
t.Errorf("неожиданный путь %q", r.URL.Path) t.Errorf("неожиданный путь %q", r.URL.Path)
} }
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"package_id":"pkg-1","state":"delivered","updated_at":"2026-03-02T14:30:00Z"}`)) _, _ = w.Write([]byte(`{"id":123,"name":"#M2MTR20260320140624.zip","status":"SENT"}`))
})) }))
defer srv.Close() defer srv.Close()
c := igw.NewClient(srv.URL, igw.WithRetry(0, time.Millisecond)) c := igw.NewClient(srv.URL, igw.WithRetry(0, time.Millisecond))
st, err := c.GetStatus(context.Background(), "pkg-1") st, err := c.GetStatus(context.Background(), "123")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if st.State != "delivered" { if st.Status != "SENT" {
t.Errorf("state = %q, ожидалось delivered", st.State) t.Errorf("status = %q, ожидалось SENT", st.Status)
}
if !strings.Contains(st.Name, "M2MTR") {
t.Errorf("name = %q, ожидалось содержать M2MTR", st.Name)
} }
} }
// TestListIncoming — формат ответа по разделу 2.6: массив пакетов с полями
// channel/id/name/type/state/files/signs.
func TestListIncoming(t *testing.T) { func TestListIncoming(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !contains(r.URL.RawQuery, "channel=TEST3") { q := r.URL.Query()
t.Errorf("в query нет channel: %s", r.URL.RawQuery) if q.Get("channel") != "CH1" {
t.Errorf("channel = %q, ожидалось CH1", q.Get("channel"))
}
if q.Get("type") != "M2MTD" {
t.Errorf("type = %q, ожидалось M2MTD", q.Get("type"))
} }
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"items":[{"package_id":"p1","package_type":"#M2MTD","channel":"TEST3","received_at":"2026-03-02T14:00:00Z","body":""}]}`)) _, _ = w.Write([]byte(`[{
"channel":"CH1",
"id":22423,
"name":"#M2MTD20260320140624.ZIP",
"type":"M2MTD",
"state":"RECEIVED",
"files":[{"id":30112,"name":"M2MTD20260320140624.XML"}],
"signs":[{"serial":"40:50:14","subject":"INN=007702165310,CN=НРД","status":"VALID"}]
}]`))
})) }))
defer srv.Close() defer srv.Close()
c := igw.NewClient(srv.URL, igw.WithRetry(0, time.Millisecond)) c := igw.NewClient(srv.URL, igw.WithRetry(0, time.Millisecond))
pkgs, err := c.ListIncoming(context.Background(), "TEST3", time.Now().Add(-time.Hour), "#M2MTD") pkgs, err := c.ListIncoming(context.Background(), igw.ListFilter{
Channel: "CH1",
Type: "M2MTD",
})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if len(pkgs) != 1 || pkgs[0].PackageType != "#M2MTD" { if len(pkgs) != 1 {
t.Errorf("неожиданный результат: %+v", pkgs) t.Fatalf("ожидался 1 пакет, получено %d", len(pkgs))
} }
body, err := pkgs[0].DecodeBody() p := pkgs[0]
if err != nil { if p.ID != 22423 {
t.Errorf("DecodeBody: %v", err) t.Errorf("ID = %d, ожидалось 22423", p.ID)
} }
if body != nil { if p.State != "RECEIVED" {
t.Errorf("ожидалось пустое тело") t.Errorf("State = %q, ожидалось RECEIVED", p.State)
}
if len(p.Signs) != 1 || p.Signs[0].Status != "VALID" {
t.Errorf("Signs неверные: %+v", p.Signs)
} }
} }
func contains(s, substr string) bool { // TestGetPackage — скачивание содержимого. ИШ может возвращать либо чистый
return len(s) >= len(substr) && (indexOf(s, substr) >= 0) // ZIP, либо JSON с base64-полем. Тестируем оба случая.
} func TestGetPackageRawZIP(t *testing.T) {
zipBytes := []byte("PK\x03\x04zip-content-here")
func indexOf(s, substr string) int { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
for i := 0; i+len(substr) <= len(s); i++ { if r.URL.Path != "/api/package/22423" {
if s[i:i+len(substr)] == substr { t.Errorf("неожиданный путь %q", r.URL.Path)
return i
} }
w.Header().Set("Content-Type", "application/zip")
_, _ = w.Write(zipBytes)
}))
defer srv.Close()
c := igw.NewClient(srv.URL, igw.WithRetry(0, time.Millisecond))
body, err := c.GetPackage(context.Background(), 22423)
if err != nil {
t.Fatal(err)
}
if string(body) != string(zipBytes) {
t.Errorf("body = %q, ожидалось %q", body, zipBytes)
}
}
func TestGetPackageBase64InJSON(t *testing.T) {
zipBytes := []byte("PK\x03\x04zip-from-base64")
encoded := base64.StdEncoding.EncodeToString(zipBytes)
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"file":"` + encoded + `"}`))
}))
defer srv.Close()
c := igw.NewClient(srv.URL, igw.WithRetry(0, time.Millisecond))
body, err := c.GetPackage(context.Background(), 1)
if err != nil {
t.Fatal(err)
}
if string(body) != string(zipBytes) {
t.Errorf("decoded = %q, ожидалось %q", body, zipBytes)
} }
return -1
} }
+173
View File
@@ -0,0 +1,173 @@
// pack.go — упаковщик/распаковщик ZIP-пакетов для ИШ НРД.
//
// Формат отправляемого пакета (раздел 2.3 инструкции):
// ZIP-архив содержит:
// - <doc>.xml — сам документ (M2MTransferRequest.xml)
// - config.xml — настроечный файл с указанием name и package
//
// Пример config.xml:
// <config>
// <name>doc.xml</name>
// <package>#M2MTR</package>
// </config>
//
// Формат входящего пакета (раздел 2.4 + раздел 2.6):
// ZIP-архив содержит:
// - <doc>.xml — сам документ (M2MTransferDecision.xml или M2MTransferResponse.xml)
// - winf.xml — транзитный конверт ЭДО НРД
// - <doc>.xml.sgn — отсоединённая подпись НРД (опц.)
//
// ИШ сам формирует пакет ЭДО (подписывает, добавляет winf.xml и т.д.).
// Наша задача — собрать ZIP с XML+config.xml и отправить.
package igw
import (
"archive/zip"
"bytes"
"encoding/xml"
"errors"
"fmt"
"io"
"path/filepath"
"strings"
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/m2m"
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/nsdxml"
)
// config — содержимое config.xml внутри пакета.
type config struct {
XMLName xml.Name `xml:"config"`
Name string `xml:"name"`
Package string `xml:"package"`
}
// PackRequest упаковывает M2MTransferRequest в ZIP-архив для ИШ.
// docFileName — имя XML внутри архива (например "M2MTransferRequest.xml").
// Возвращает байты ZIP, готовые к отправке через POST /api/package/{channel}/file.
func PackRequest(req *m2m.M2MTransferRequest, docFileName string) ([]byte, error) {
if req == nil {
return nil, errors.New("igw: PackRequest: req=nil")
}
if docFileName == "" {
docFileName = "M2MTransferRequest.xml"
}
xmlBytes, err := nsdxml.Marshal(req)
if err != nil {
return nil, fmt.Errorf("igw: marshal M2MTransferRequest: %w", err)
}
return packZIP(xmlBytes, docFileName, "#M2MTR")
}
// PackXML упаковывает произвольный XML в ZIP с config.xml. Тип пакета
// (например "#M2MTR" / "#M2MTD") задаётся явно. Полезно когда XML уже
// собран снаружи (тесты, эталонные сообщения из инструкции).
func PackXML(xmlBytes []byte, docFileName, packageType string) ([]byte, error) {
if len(xmlBytes) == 0 {
return nil, errors.New("igw: PackXML: xmlBytes пустой")
}
if !strings.HasPrefix(packageType, "#") {
return nil, fmt.Errorf("igw: packageType должен начинаться с #, получено %q", packageType)
}
if docFileName == "" {
docFileName = "doc.xml"
}
return packZIP(xmlBytes, docFileName, packageType)
}
func packZIP(xmlBytes []byte, docFileName, packageType string) ([]byte, error) {
var buf bytes.Buffer
w := zip.NewWriter(&buf)
// 1. сам документ
fw, err := w.Create(docFileName)
if err != nil {
return nil, fmt.Errorf("igw: create %s in zip: %w", docFileName, err)
}
if _, err := fw.Write(xmlBytes); err != nil {
return nil, err
}
// 2. config.xml
cfg := config{Name: docFileName, Package: packageType}
cfgBytes, err := xml.MarshalIndent(cfg, "", " ")
if err != nil {
return nil, fmt.Errorf("igw: marshal config.xml: %w", err)
}
cfgWriter, err := w.Create("config.xml")
if err != nil {
return nil, fmt.Errorf("igw: create config.xml in zip: %w", err)
}
if _, err := cfgWriter.Write(cfgBytes); err != nil {
return nil, err
}
if err := w.Close(); err != nil {
return nil, fmt.Errorf("igw: close zip: %w", err)
}
return buf.Bytes(), nil
}
// UnpackedPackage — содержимое распакованного ZIP'а с входящим пакетом.
type UnpackedPackage struct {
DocXML []byte // первый XML, который не winf.xml и не config.xml
WinfXML []byte // транзитный конверт ЭДО (опц., присутствует у входящих от НРД)
Signature []byte // .sgn файл (отсоединённая подпись), опц.
Filenames []string
}
// UnpackPackage распаковывает ZIP-архив от ИШ и возвращает структурированно
// тело документа + winf.xml + отсоединённую подпись.
func UnpackPackage(zipBytes []byte) (*UnpackedPackage, error) {
r, err := zip.NewReader(bytes.NewReader(zipBytes), int64(len(zipBytes)))
if err != nil {
return nil, fmt.Errorf("igw: zip reader: %w", err)
}
out := &UnpackedPackage{}
for _, f := range r.File {
out.Filenames = append(out.Filenames, f.Name)
rc, err := f.Open()
if err != nil {
return nil, fmt.Errorf("igw: open %s in zip: %w", f.Name, err)
}
data, err := io.ReadAll(rc)
rc.Close()
if err != nil {
return nil, fmt.Errorf("igw: read %s from zip: %w", f.Name, err)
}
low := strings.ToLower(filepath.Base(f.Name))
switch {
case low == "winf.xml":
out.WinfXML = data
case low == "config.xml":
// config.xml в исходящих, во входящих обычно отсутствует — игнорируем
case strings.HasSuffix(low, ".sgn"):
out.Signature = data
case strings.HasSuffix(low, ".xml") && out.DocXML == nil:
out.DocXML = data
}
}
if out.DocXML == nil {
return nil, fmt.Errorf("igw: в ZIP нет основного .xml документа (файлы: %v)", out.Filenames)
}
return out, nil
}
// ParseDecision разбирает DocXML входящего пакета M2MTD в m2m.M2MTransferDecision.
func ParseDecision(docXML []byte) (*m2m.M2MTransferDecision, error) {
var d m2m.M2MTransferDecision
if err := nsdxml.Unmarshal(docXML, &d); err != nil {
return nil, fmt.Errorf("igw: parse M2MTransferDecision: %w", err)
}
return &d, nil
}
// ParseResponse разбирает DocXML входящего пакета M2MER в m2m.M2MTransferResponse.
func ParseResponse(docXML []byte) (*m2m.M2MTransferResponse, error) {
var r m2m.M2MTransferResponse
if err := nsdxml.Unmarshal(docXML, &r); err != nil {
return nil, fmt.Errorf("igw: parse M2MTransferResponse: %w", err)
}
return &r, nil
}
+114
View File
@@ -0,0 +1,114 @@
package igw_test
import (
"archive/zip"
"bytes"
"io"
"strings"
"testing"
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/nsdadapter/igw"
)
// TestPackXML_StructureMatchesSpec — после упаковки в ZIP должны быть
// ровно два файла: doc.xml + config.xml. Config содержит <name> и
// <package>. Это структура из раздела 2.3 инструкции НРД.
func TestPackXML_StructureMatchesSpec(t *testing.T) {
xmlBody := []byte(`<?xml version="1.0" encoding="windows-1251"?><rt:M2MTransferRequest/>`)
zipBytes, err := igw.PackXML(xmlBody, "M2MTransferRequest.xml", "#M2MTR")
if err != nil {
t.Fatal(err)
}
r, err := zip.NewReader(bytes.NewReader(zipBytes), int64(len(zipBytes)))
if err != nil {
t.Fatal(err)
}
if len(r.File) != 2 {
t.Fatalf("в ZIP должно быть 2 файла, получено %d", len(r.File))
}
got := map[string][]byte{}
for _, f := range r.File {
rc, _ := f.Open()
b, _ := io.ReadAll(rc)
rc.Close()
got[f.Name] = b
}
if _, ok := got["M2MTransferRequest.xml"]; !ok {
t.Errorf("в ZIP нет M2MTransferRequest.xml. Файлы: %v", keys(got))
}
cfg, ok := got["config.xml"]
if !ok {
t.Fatalf("в ZIP нет config.xml. Файлы: %v", keys(got))
}
cfgStr := string(cfg)
if !strings.Contains(cfgStr, "<name>M2MTransferRequest.xml</name>") {
t.Errorf("config.xml не содержит правильное <name>: %s", cfgStr)
}
if !strings.Contains(cfgStr, "<package>#M2MTR</package>") {
t.Errorf("config.xml не содержит правильное <package>: %s", cfgStr)
}
}
func TestPackXML_RejectsBadPackageType(t *testing.T) {
_, err := igw.PackXML([]byte("<x/>"), "doc.xml", "M2MTR")
if err == nil {
t.Fatal("ожидалась ошибка для packageType без #")
}
}
// TestUnpackPackage_FindsXMLAndWinf — распаковка эмулирует входящий ZIP
// от ИШ: M2MTD.xml + winf.xml + .sgn. Проверяем что UnpackPackage
// корректно раскладывает по полям.
func TestUnpackPackage_FindsXMLAndWinf(t *testing.T) {
docBody := []byte("<dn:M2MTransferDecision/>")
winfBody := []byte("<winf/>")
sgnBody := []byte("BINARY-SIGN-BLOB")
var buf bytes.Buffer
w := zip.NewWriter(&buf)
must := func(name string, data []byte) {
fw, _ := w.Create(name)
_, _ = fw.Write(data)
}
must("M2MTD20260320140624.XML", docBody)
must("winf.xml", winfBody)
must("M2MTD20260320140624.XML.sgn", sgnBody)
_ = w.Close()
pkg, err := igw.UnpackPackage(buf.Bytes())
if err != nil {
t.Fatal(err)
}
if string(pkg.DocXML) != string(docBody) {
t.Errorf("DocXML mismatch")
}
if string(pkg.WinfXML) != string(winfBody) {
t.Errorf("WinfXML mismatch")
}
if string(pkg.Signature) != string(sgnBody) {
t.Errorf("Signature mismatch")
}
if len(pkg.Filenames) != 3 {
t.Errorf("ожидалось 3 файла в Filenames, получено %d", len(pkg.Filenames))
}
}
func TestUnpackPackage_EmptyXML(t *testing.T) {
var buf bytes.Buffer
w := zip.NewWriter(&buf)
fw, _ := w.Create("winf.xml")
_, _ = fw.Write([]byte("<winf/>"))
_ = w.Close()
_, err := igw.UnpackPackage(buf.Bytes())
if err == nil {
t.Fatal("ожидалась ошибка когда в ZIP нет основного .xml")
}
}
func keys(m map[string][]byte) []string {
out := make([]string, 0, len(m))
for k := range m {
out = append(out, k)
}
return out
}