Compare commits
5 Commits
5fa6ea6ab1
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 5440ebe152 | |||
| 6e503433d4 | |||
| bac55cbdfd | |||
| 7a7aa0cf6c | |||
| de41aea00c |
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Спросить пользователя, если непонятно** — не стесняйся. Лучше задать вопрос чем сделать неверное предположение.
|
||||||
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.
@@ -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 учётка.
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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 МОСТ.** На нашей стороне всё уже готово — задержки только во внешних запросах.
|
||||||
Executable
+114
@@ -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 "================================================================"
|
||||||
Executable
+134
@@ -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 "================================================================"
|
||||||
Executable
+109
@@ -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 "================================================================"
|
||||||
Executable
+89
@@ -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 "================================================================"
|
||||||
Executable
+384
@@ -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
|
||||||
Vendored
+62
@@ -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
|
||||||
@@ -20,7 +20,7 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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}}
|
||||||
@@ -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"`
|
|
||||||
PackageType string `json:"package_type"`
|
|
||||||
Channel string `json:"channel"`
|
Channel string `json:"channel"`
|
||||||
ReceivedAt time.Time `json:"received_at"`
|
ID int `json:"id"`
|
||||||
Body string `json:"body,omitempty"` // base64
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"` // "M2MTD" | "M2MER"
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 p.ID != 22423 {
|
||||||
|
t.Errorf("ID = %d, ожидалось 22423", p.ID)
|
||||||
|
}
|
||||||
|
if p.State != "RECEIVED" {
|
||||||
|
t.Errorf("State = %q, ожидалось RECEIVED", p.State)
|
||||||
|
}
|
||||||
|
if len(p.Signs) != 1 || p.Signs[0].Status != "VALID" {
|
||||||
|
t.Errorf("Signs неверные: %+v", p.Signs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestGetPackage — скачивание содержимого. ИШ может возвращать либо чистый
|
||||||
|
// ZIP, либо JSON с base64-полем. Тестируем оба случая.
|
||||||
|
func TestGetPackageRawZIP(t *testing.T) {
|
||||||
|
zipBytes := []byte("PK\x03\x04zip-content-here")
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.Path != "/api/package/22423" {
|
||||||
|
t.Errorf("неожиданный путь %q", r.URL.Path)
|
||||||
|
}
|
||||||
|
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 {
|
if err != nil {
|
||||||
t.Errorf("DecodeBody: %v", err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if body != nil {
|
if string(body) != string(zipBytes) {
|
||||||
t.Errorf("ожидалось пустое тело")
|
t.Errorf("body = %q, ожидалось %q", body, zipBytes)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func contains(s, substr string) bool {
|
func TestGetPackageBase64InJSON(t *testing.T) {
|
||||||
return len(s) >= len(substr) && (indexOf(s, substr) >= 0)
|
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) {
|
||||||
func indexOf(s, substr string) int {
|
t.Errorf("decoded = %q, ожидалось %q", body, zipBytes)
|
||||||
for i := 0; i+len(substr) <= len(s); i++ {
|
|
||||||
if s[i:i+len(substr)] == substr {
|
|
||||||
return i
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return -1
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user