9737c787f9
Инфраструктура M2M (живой обмен с НРД через ИШ): - обработка M2MTransferResponse: ERROR(M2Mxx) → заявка Отклонена, сохранение ответа; INFO → ждём Decision; идемпотентность поллера - fallback-корреляция ответов с нулевым GUID (M2M14/M2M17) по FIFO - сырой XML ответа НРД в карточке заявки (для пересылки в ТП) - тестовый пакет роботу приведён к эталону m2m_robot_samples (CostInfo=Yes, 4 бумаги, IsolationStatus, DocumentSeries=сценарий); override паспорта - редирект из теста сразу в карточку заявки Мастер установки ключа Валидаты на флешку (admin/setup/keywizard): - пошаговый: загрузка .7z+пароль → выбор флешки → запись → справочник сертификатов (CRL) → перезапуск+проверка ИШ → готово - привилегированный воркер (bj-keymedia) в host-namespace через файл-обмен, bj-server остаётся в песочнице - сохранение структуры профиля архива (spr<N>), перечисление съёмных USB Прочее: - пакет-доказательство для ТП НРД + форма регистрации участника M2M - эталонные образцы робота (DOC/m2m_robot_samples) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
189 lines
5.7 KiB
Go
189 lines
5.7 KiB
Go
package lkgateway
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"net"
|
||
"net/http"
|
||
|
||
"github.com/jackc/pgx/v5/pgxpool"
|
||
"os"
|
||
"time"
|
||
)
|
||
|
||
// Status — состояние одной проверяемой подсистемы.
|
||
type Status struct {
|
||
Name string `json:"name"`
|
||
OK bool `json:"ok"`
|
||
Message string `json:"message,omitempty"`
|
||
Detail string `json:"detail,omitempty"`
|
||
// Optional — компонент не обязателен для работы с НРД. Его «не-OK»
|
||
// не делает систему «не готовой» (напр. callback в ЛК).
|
||
Optional bool `json:"optional,omitempty"`
|
||
}
|
||
|
||
// SystemStatus — все проверки.
|
||
type SystemStatus struct {
|
||
Profile string `json:"profile"`
|
||
Provider string `json:"crypto_provider"`
|
||
Checks []Status `json:"checks"`
|
||
CheckedAt time.Time `json:"checked_at"`
|
||
}
|
||
|
||
// CheckOptions — что и как проверять.
|
||
type CheckOptions struct {
|
||
PostgresDSN string // если пусто — режим in-memory, проверки нет
|
||
CryptoSocket string // путь до UDS crypto-service
|
||
NSDAdapterURL string // например http://127.0.0.1:8082
|
||
LKCallbackURL string // куда шлём callback (lk-emulator)
|
||
Profile string // имя профиля nsdadapter (guest-gost...)
|
||
CryptoProvider string // BJ_CRYPTO_PROVIDER (stub|cryptopro|...)
|
||
Timeout time.Duration // таймаут на одну проверку
|
||
}
|
||
|
||
// CheckAll выполняет все доступные проверки и возвращает SystemStatus.
|
||
func CheckAll(ctx context.Context, o CheckOptions) SystemStatus {
|
||
if o.Timeout == 0 {
|
||
o.Timeout = 2 * time.Second
|
||
}
|
||
out := SystemStatus{
|
||
Profile: o.Profile,
|
||
Provider: o.CryptoProvider,
|
||
CheckedAt: time.Now().UTC(),
|
||
}
|
||
|
||
out.Checks = append(out.Checks, checkPostgres(ctx, o))
|
||
out.Checks = append(out.Checks, checkCryptoSocket(o))
|
||
out.Checks = append(out.Checks, checkNSDAdapter(ctx, o))
|
||
out.Checks = append(out.Checks, checkLKCallback(ctx, o))
|
||
|
||
return out
|
||
}
|
||
|
||
func checkPostgres(ctx context.Context, o CheckOptions) Status {
|
||
s := Status{Name: "База данных PostgreSQL"}
|
||
if o.PostgresDSN == "" {
|
||
s.OK = true
|
||
s.Optional = true
|
||
s.Message = "in-memory — данные не сохраняются между перезапусками"
|
||
return s
|
||
}
|
||
pctx, cancel := context.WithTimeout(ctx, o.Timeout)
|
||
defer cancel()
|
||
pool, err := pgxpool.New(pctx, o.PostgresDSN)
|
||
if err != nil {
|
||
s.OK = false
|
||
s.Message = "ошибка подключения: " + err.Error()
|
||
return s
|
||
}
|
||
defer pool.Close()
|
||
if err := pool.Ping(pctx); err != nil {
|
||
s.OK = false
|
||
s.Message = "не отвечает: " + err.Error()
|
||
return s
|
||
}
|
||
s.OK = true
|
||
s.Message = "подключена, репозиторий m2m_core.deals"
|
||
return s
|
||
}
|
||
|
||
func checkCryptoSocket(o CheckOptions) Status {
|
||
s := Status{Name: "crypto-service (UDS)"}
|
||
if o.CryptoSocket == "" {
|
||
s.OK = false
|
||
s.Message = "BJ_CRYPTO_SOCKET не задан"
|
||
return s
|
||
}
|
||
info, err := os.Stat(o.CryptoSocket)
|
||
if err != nil {
|
||
s.OK = false
|
||
s.Message = "сокет недоступен"
|
||
s.Detail = err.Error()
|
||
return s
|
||
}
|
||
if info.Mode()&os.ModeSocket == 0 {
|
||
s.OK = false
|
||
s.Message = "путь существует, но это не сокет"
|
||
s.Detail = o.CryptoSocket
|
||
return s
|
||
}
|
||
// Пробуем подключиться.
|
||
d := net.Dialer{Timeout: o.Timeout}
|
||
conn, err := d.Dial("unix", o.CryptoSocket)
|
||
if err != nil {
|
||
s.OK = false
|
||
s.Message = "сокет существует, но не отвечает"
|
||
s.Detail = err.Error()
|
||
return s
|
||
}
|
||
_ = conn.Close()
|
||
s.OK = true
|
||
s.Message = "сокет открыт"
|
||
s.Detail = o.CryptoSocket
|
||
if o.CryptoProvider == "stub" || o.CryptoProvider == "" {
|
||
s.Message += ", провайдер stub (реальная криптография не подключена)"
|
||
} else {
|
||
s.Message += ", провайдер " + o.CryptoProvider
|
||
}
|
||
return s
|
||
}
|
||
|
||
func checkNSDAdapter(ctx context.Context, o CheckOptions) Status {
|
||
s := Status{Name: "Интеграционный шлюз НРД"}
|
||
if o.NSDAdapterURL == "" {
|
||
s.OK = true
|
||
s.Optional = true
|
||
s.Message = "не подключён — режим эмуляции (mock)"
|
||
return s
|
||
}
|
||
// У ИШ нет /healthz — проверяем рабочий эндпоинт Web API (engine/state
|
||
// отвечает 200 «Running», когда движок поднят).
|
||
st := httpHealth(ctx, o.NSDAdapterURL+"/api/admin/engine/state", o.Timeout, s)
|
||
if st.OK {
|
||
st.Message = "подключён, движок работает"
|
||
}
|
||
return st
|
||
}
|
||
|
||
func checkLKCallback(ctx context.Context, o CheckOptions) Status {
|
||
s := Status{Name: "Callback в личный кабинет", Optional: true}
|
||
if o.LKCallbackURL == "" {
|
||
s.OK = false
|
||
s.Message = "не настроен — уведомления в ЛК отключены (необязательно для работы с НРД)"
|
||
return s
|
||
}
|
||
return httpHealth(ctx, o.LKCallbackURL+"/healthz", o.Timeout, s)
|
||
}
|
||
|
||
func httpHealth(ctx context.Context, url string, timeout time.Duration, s Status) Status {
|
||
c := &http.Client{Timeout: timeout}
|
||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||
if err != nil {
|
||
s.OK = false
|
||
s.Message = "не получилось собрать запрос"
|
||
s.Detail = err.Error()
|
||
return s
|
||
}
|
||
resp, err := c.Do(req)
|
||
if err != nil {
|
||
s.OK = false
|
||
s.Message = "недоступен"
|
||
s.Detail = err.Error()
|
||
return s
|
||
}
|
||
defer resp.Body.Close()
|
||
if resp.StatusCode != http.StatusOK {
|
||
s.OK = false
|
||
s.Message = "HTTP " + http.StatusText(resp.StatusCode)
|
||
s.Detail = url
|
||
return s
|
||
}
|
||
s.OK = true
|
||
s.Message = "OK"
|
||
s.Detail = url
|
||
return s
|
||
}
|
||
|
||
// ErrUnknown — общий placeholder.
|
||
var ErrUnknown = errors.New("lkgateway: unknown error")
|