Files
Bridge-and-Join-s/internal/lkgateway/checks.go
T
zuevav 773fa8e0bf Мастер: шапка на месте; домен — только логин и пароль; модуль «1С: база мидл-офиса» (тест)
- Шапка мастера: правый блок (шлюз НРД, пользователь, тема) снова справа — после правила [hidden] скрытое меню перестало его распирать.
- Домен в шаге «Учётные записи»: bj сам находит домен и контроллеры (логин с @домен, домены поиска DNS, rootDSE DNS-серверов, SRV _ldap._tcp.dc._msdcs); адрес и домен — в расширенных настройках, открываются сами, если не нашлось. Контроллеры по очереди, LDAPS → LDAP+StartTLS.
- Неудачная попытка ничего не теряет: введённое (и пароль) — черновиком, форма заполнена; причины словами (неизвестный ЦС, имя в сертификате, IP вместо имени, порт закрыт, нет в DNS).
- Сертификат неизвестного центра: карточка «кем выдан, отпечаток (SHA-1 как в Windows и SHA-256)» и «Доверять и подключить»; после входа bj берёт из домена сертификат центра (AIA / Certification Authorities), подписавший сертификат контроллера, — подходят все контроллеры. То же в Настройка → Пользователи при «Проверить».
- Лицензия: модуль onec-middle «1С: база мидл-офиса (тест)»; раздел настроек — адрес публикации, пользователь, пароль, проверка через OData $metadata; в мастере — в «Модулях».
- ЛК (callback, API, готовность) — только при модуле «ЕСИА Финанс»; карточка режима QUIK и стили настроек как в мастере.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
2026-10-07 01:03:16 +03:00

256 lines
8.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package lkgateway
import (
"context"
"errors"
"net"
"net/http"
neturl "net/url"
"strings"
"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, ishVersionStatus(ctx))
if globalRC == nil || moduleAllowed(globalRC, "esia-finance") || devMode(globalRC) { // без модуля «ЕСИА Финанс» нечего проверять
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 == "" {
if devMode(globalRC) {
s.Message += ", провайдер stub (реальная криптография не подключена)"
} else {
s.Message += ", средство подписи не настроено"
}
} 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 = "не настроен — обмен с НРД не выполняется"
if devMode(globalRC) {
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 {
// Подключаемся по реальному url (для локальных сервисов — loopback), но в
// Detail показываем адрес/имя сервера, а не «localhost» — так оператору
// понятно, на каком узле работает компонент. Для loopback добавляем пометку,
// что порт не открыт наружу (это подпись, а не кликабельная ссылка).
disp := displayURL(url)
if isLoopbackURL(url) {
disp += " (только локально — доступ с сервера или через SSH-туннель, порт не открыт наружу)"
}
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 = disp
return s
}
s.OK = true
s.Message = "OK"
s.Detail = disp
return s
}
// displayHost возвращает отображаемое имя/адрес этого сервера: значение
// BJ_PUBLIC_HOST (если задано, напр. DNS-имя) либо системный hostname.
func displayHost() string {
if h := strings.TrimSpace(os.Getenv("BJ_PUBLIC_HOST")); h != "" {
return h
}
if h, err := os.Hostname(); err == nil && h != "" {
return h
}
return "server"
}
// isLoopbackURL сообщает, указывает ли url на loopback-хост (сервис слушает
// только на этом сервере и недоступен извне).
func isLoopbackURL(raw string) bool {
u, err := neturl.Parse(raw)
if err != nil {
return false
}
switch u.Hostname() {
case "localhost", "127.0.0.1", "0.0.0.0", "::1":
return true
}
return false
}
// displayURL заменяет loopback-хост (localhost/127.0.0.1/0.0.0.0/[::1]) в url на
// displayHost(). Внешние адреса не трогает. Порт и путь сохраняются.
func displayURL(raw string) string {
u, err := neturl.Parse(raw)
if err != nil || u.Host == "" {
return raw
}
host := u.Hostname()
switch host {
case "localhost", "127.0.0.1", "0.0.0.0", "::1":
newHost := displayHost()
if p := u.Port(); p != "" {
u.Host = net.JoinHostPort(newHost, p)
} else {
u.Host = newHost
}
return u.String()
}
return raw
}
// ErrUnknown — общий placeholder.
var ErrUnknown = errors.New("lkgateway: unknown error")