feat(bj): модуль ЛК на PostgreSQL, обмен с DEPO, боевой ЛК с имитатором НРД, QUIK, НПР
- bj_exchange и «кран» Fancy убраны: данные модуля ЛК в PostgreSQL (схема lk) - трансферные таблицы FTV_M2M: создание при установке, события, справочник участников - сверка с DEPO: счёт, ФИО, дата рождения, ИИС по договору, квал-инвестор из опции DEPO - боевой ЛК ЕСИА Финанс при тестовом ИШ: журнал, отказ через DEPO, статусы в ЛК - онлайн-лицензирование в bj, план тестирования M2M для контрагента (интерактивный HTML) - модули QUIK МНП, экспорт QUIK, НПР/УДС; документы и тестовые материалы Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,609 @@
|
||||
package lkgateway
|
||||
|
||||
// Доверенные сертификаты НРД в справочнике сертификатов активного профиля ИШ.
|
||||
//
|
||||
// НРД периодически меняет ключ крипто-сервера (например, в ночь на 10.09.2026 —
|
||||
// сертификат Шляппо А.А. от 21.08.2026). Участник обязан ДО смены добавить
|
||||
// новый сертификат в локальный справочник, а старый удалить в разрешённое окно.
|
||||
// Вручную это делают через GUI «Справочник сертификатов»; у нас ИШ работает
|
||||
// без GUI, поэтому bj делает это сам:
|
||||
//
|
||||
// - комплект сертификатов + manifest.json ВШИТ в бинарь (go:embed nsd-certs/),
|
||||
// т.е. приезжает всем клиентам вместе с обновлением bj;
|
||||
// - дополнительно читаются ручные .cer из /var/lib/bj/.bj/nsd-certs/;
|
||||
// - root-воркер (bj-keymedia-install certs-list / certs-apply) сверяет и
|
||||
// пересобирает справочник профиля: бэкап → пересборка → проверка подписи →
|
||||
// откат при ошибке → перезапуск ИШ;
|
||||
// - сертификат добавляется ТОЛЬКО если его УЦ уже есть в справочнике —
|
||||
// боевой сертификат никогда не попадёт в тестовый профиль и наоборот;
|
||||
// - старый сертификат удаляется не раньше remove_replaced_after из манифеста.
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/x509/pkix"
|
||||
"embed"
|
||||
"encoding/asn1"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
//go:embed nsd-certs/*
|
||||
var nsdCertsFS embed.FS
|
||||
|
||||
const (
|
||||
nsdCertsDropinDir = "/var/lib/bj/.bj/nsd-certs" // ручные .cer оператора
|
||||
nsdCertsApplyDir = "/var/lib/bj/nsd-certs-apply" // staging для root-воркера
|
||||
nsdCertsInterval = 6 * time.Hour
|
||||
nsdCertsStartWait = 90 * time.Second
|
||||
)
|
||||
|
||||
// NSDCertsSettings — состояние автообновления доверенных сертификатов НРД.
|
||||
type NSDCertsSettings struct {
|
||||
Manual bool `json:"manual"` // true = только по кнопке «Применить»
|
||||
LastRun time.Time `json:"last_run"`
|
||||
LastLog string `json:"last_log"`
|
||||
LastProfile string `json:"last_profile"`
|
||||
LastRows []NSDCertRow `json:"last_rows"`
|
||||
LastStore []NSDStoreCert `json:"last_store"`
|
||||
LastApplied time.Time `json:"last_applied"`
|
||||
}
|
||||
|
||||
// UpdateNSDCerts сохраняет состояние автообновления сертификатов НРД.
|
||||
func (r *RuntimeConfig) UpdateNSDCerts(s NSDCertsSettings) error {
|
||||
r.mu.Lock()
|
||||
r.data.NSDCerts = s
|
||||
r.data.UpdatedAt = time.Now()
|
||||
r.mu.Unlock()
|
||||
return r.save()
|
||||
}
|
||||
|
||||
// ---- манифест и разбор сертификатов ----
|
||||
|
||||
type nsdManifest struct {
|
||||
Certs []nsdManifestCert `json:"certs"`
|
||||
}
|
||||
|
||||
type nsdManifestCert struct {
|
||||
File string `json:"file"`
|
||||
Title string `json:"title"`
|
||||
Contour string `json:"contour"` // prod | test | manual
|
||||
Serial string `json:"serial"`
|
||||
ReplacesSerial string `json:"replaces_serial"`
|
||||
ReplacesTitle string `json:"replaces_title"`
|
||||
RemoveReplacedAfter string `json:"remove_replaced_after"` // RFC3339
|
||||
Source string `json:"source"`
|
||||
Note string `json:"note"`
|
||||
}
|
||||
|
||||
type nsdCertInfo struct {
|
||||
SubjectCN, SubjectO, IssuerCN, Serial string
|
||||
NotBefore, NotAfter time.Time
|
||||
SelfSigned bool
|
||||
}
|
||||
|
||||
type nsdBundleCert struct {
|
||||
nsdManifestCert
|
||||
Info nsdCertInfo
|
||||
DER []byte
|
||||
Origin string // bundle | dropin
|
||||
}
|
||||
|
||||
// asn1-структуры: разбираем только TBS-заголовок — стандартная x509 не
|
||||
// понимает ГОСТ-ключи («malformed certificate»), а нам нужны только
|
||||
// subject/issuer/serial/validity. Хвост SEQUENCE Go asn1 допускает.
|
||||
type nsdASN1Validity struct{ NotBefore, NotAfter time.Time }
|
||||
type nsdASN1TBS struct {
|
||||
Version int `asn1:"optional,explicit,default:0,tag:0"`
|
||||
Serial *big.Int
|
||||
SigAlg asn1.RawValue
|
||||
Issuer asn1.RawValue
|
||||
Validity nsdASN1Validity
|
||||
Subject asn1.RawValue
|
||||
}
|
||||
type nsdASN1Cert struct {
|
||||
TBS nsdASN1TBS
|
||||
SigAlg asn1.RawValue
|
||||
Sig asn1.BitString
|
||||
}
|
||||
|
||||
func nsdName(raw asn1.RawValue) (cn, o string) {
|
||||
var rdn pkix.RDNSequence
|
||||
if _, err := asn1.Unmarshal(raw.FullBytes, &rdn); err != nil {
|
||||
return "", ""
|
||||
}
|
||||
var n pkix.Name
|
||||
n.FillFromRDNSequence(&rdn)
|
||||
if len(n.Organization) > 0 {
|
||||
o = n.Organization[0]
|
||||
}
|
||||
return n.CommonName, o
|
||||
}
|
||||
|
||||
// parseNSDCert принимает DER или PEM, возвращает сведения + DER.
|
||||
func parseNSDCert(raw []byte) (nsdCertInfo, []byte, error) {
|
||||
der := raw
|
||||
if bytes.Contains(raw, []byte("-----BEGIN")) {
|
||||
blk, _ := pem.Decode(raw)
|
||||
if blk == nil {
|
||||
return nsdCertInfo{}, nil, fmt.Errorf("PEM не разобран")
|
||||
}
|
||||
der = blk.Bytes
|
||||
}
|
||||
var c nsdASN1Cert
|
||||
if _, err := asn1.Unmarshal(der, &c); err != nil {
|
||||
return nsdCertInfo{}, nil, fmt.Errorf("ASN.1: %v", err)
|
||||
}
|
||||
info := nsdCertInfo{Serial: fmt.Sprintf("%X", c.TBS.Serial), NotBefore: c.TBS.Validity.NotBefore, NotAfter: c.TBS.Validity.NotAfter}
|
||||
info.SubjectCN, info.SubjectO = nsdName(c.TBS.Subject)
|
||||
info.IssuerCN, _ = nsdName(c.TBS.Issuer)
|
||||
info.SelfSigned = bytes.Equal(c.TBS.Subject.FullBytes, c.TBS.Issuer.FullBytes)
|
||||
return info, der, nil
|
||||
}
|
||||
|
||||
func normSerial(s string) string {
|
||||
s = strings.ToUpper(strings.TrimSpace(s))
|
||||
s = strings.TrimPrefix(s, "0X")
|
||||
return strings.TrimLeft(s, "0")
|
||||
}
|
||||
|
||||
// loadNSDCertBundle — вшитый комплект + ручные .cer оператора.
|
||||
func loadNSDCertBundle() ([]nsdBundleCert, []string) {
|
||||
var out []nsdBundleCert
|
||||
var warns []string
|
||||
mb, err := nsdCertsFS.ReadFile("nsd-certs/manifest.json")
|
||||
if err != nil {
|
||||
warns = append(warns, "манифест не найден: "+err.Error())
|
||||
}
|
||||
var m nsdManifest
|
||||
if err == nil {
|
||||
if jerr := json.Unmarshal(mb, &m); jerr != nil {
|
||||
warns = append(warns, "манифест не разобран: "+jerr.Error())
|
||||
}
|
||||
}
|
||||
for _, mc := range m.Certs {
|
||||
raw, err := nsdCertsFS.ReadFile("nsd-certs/" + mc.File)
|
||||
if err != nil {
|
||||
warns = append(warns, mc.File+": "+err.Error())
|
||||
continue
|
||||
}
|
||||
info, der, err := parseNSDCert(raw)
|
||||
if err != nil {
|
||||
warns = append(warns, mc.File+": "+err.Error())
|
||||
continue
|
||||
}
|
||||
if mc.Serial != "" && normSerial(mc.Serial) != normSerial(info.Serial) {
|
||||
warns = append(warns, fmt.Sprintf("%s: серийный в манифесте %s ≠ в файле %s — беру из файла", mc.File, mc.Serial, info.Serial))
|
||||
}
|
||||
mc.Serial = info.Serial
|
||||
if mc.Title == "" {
|
||||
mc.Title = info.SubjectCN
|
||||
}
|
||||
out = append(out, nsdBundleCert{nsdManifestCert: mc, Info: info, DER: der, Origin: "bundle"})
|
||||
}
|
||||
// ручные сертификаты оператора — без замен/удалений
|
||||
if files, _ := filepath.Glob(filepath.Join(nsdCertsDropinDir, "*.cer")); len(files) > 0 {
|
||||
sort.Strings(files)
|
||||
for _, f := range files {
|
||||
raw, err := os.ReadFile(f)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
info, der, err := parseNSDCert(raw)
|
||||
if err != nil {
|
||||
warns = append(warns, filepath.Base(f)+": "+err.Error())
|
||||
continue
|
||||
}
|
||||
dup := false
|
||||
for _, b := range out {
|
||||
if normSerial(b.Serial) == normSerial(info.Serial) {
|
||||
dup = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if dup {
|
||||
continue
|
||||
}
|
||||
out = append(out, nsdBundleCert{
|
||||
nsdManifestCert: nsdManifestCert{File: filepath.Base(f), Title: info.SubjectCN + " (ручной, " + filepath.Base(f) + ")", Contour: "manual", Serial: info.Serial},
|
||||
Info: info, DER: der, Origin: "dropin",
|
||||
})
|
||||
}
|
||||
}
|
||||
return out, warns
|
||||
}
|
||||
|
||||
// ---- содержимое справочника (от root-воркера) ----
|
||||
|
||||
// NSDStoreCert — запись справочника профиля ИШ (JSON от certs-list).
|
||||
type NSDStoreCert struct {
|
||||
Kind string `json:"kind"` // x509 | crl
|
||||
Store string `json:"store"`
|
||||
Subject string `json:"subject"`
|
||||
SubjectCN string `json:"subject_cn"`
|
||||
IssuerCN string `json:"issuer_cn"`
|
||||
Serial string `json:"serial"`
|
||||
NotBefore string `json:"not_before"`
|
||||
NotAfter string `json:"not_after"`
|
||||
SelfSigned bool `json:"self_signed"`
|
||||
}
|
||||
|
||||
func nsdStoreFromResult(res map[string]any) []NSDStoreCert {
|
||||
b, _ := json.Marshal(res["certs"])
|
||||
var st []NSDStoreCert
|
||||
_ = json.Unmarshal(b, &st)
|
||||
for i := range st {
|
||||
st[i].NotBefore = nsdPrettyDate(st[i].NotBefore)
|
||||
st[i].NotAfter = nsdPrettyDate(st[i].NotAfter)
|
||||
}
|
||||
return st
|
||||
}
|
||||
|
||||
func nsdPrettyDate(s string) string {
|
||||
if t, err := time.Parse("Jan 2 15:04:05 2006 MST", strings.Join(strings.Fields(s), " ")); err == nil {
|
||||
return t.Format("02.01.2006")
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func workerCertsList(ctx context.Context) (string, []NSDStoreCert, error) {
|
||||
res, err := runKeymediaOp(ctx, "certs-list", nil)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
prof, _ := res["profile"].(string)
|
||||
return prof, nsdStoreFromResult(res), nil
|
||||
}
|
||||
|
||||
// ---- план ----
|
||||
|
||||
// NSDCertRow — строка таблицы «комплект ↔ справочник».
|
||||
type NSDCertRow struct {
|
||||
Title string `json:"title"`
|
||||
Contour string `json:"contour"`
|
||||
SubjectCN string `json:"subject_cn"`
|
||||
IssuerCN string `json:"issuer_cn"`
|
||||
Serial string `json:"serial"`
|
||||
ValidFrom string `json:"valid_from"`
|
||||
ValidTo string `json:"valid_to"`
|
||||
Status string `json:"status"`
|
||||
Kind string `json:"kind"` // ok | add | skip | expired | unknown
|
||||
Action string `json:"action,omitempty"`
|
||||
RemoveNote string `json:"remove_note,omitempty"`
|
||||
RemoveSerial string `json:"remove_serial,omitempty"`
|
||||
bundleIdx int
|
||||
}
|
||||
|
||||
func nsdCertsPlan(bundle []nsdBundleCert, store []NSDStoreCert, now time.Time) []NSDCertRow {
|
||||
leaf := map[string]NSDStoreCert{}
|
||||
ca := map[string]bool{}
|
||||
for _, c := range store {
|
||||
if c.Kind != "x509" {
|
||||
continue
|
||||
}
|
||||
leaf[normSerial(c.Serial)] = c
|
||||
if c.SelfSigned {
|
||||
ca[strings.TrimSpace(c.SubjectCN)] = true
|
||||
}
|
||||
}
|
||||
var rows []NSDCertRow
|
||||
for i, b := range bundle {
|
||||
row := NSDCertRow{
|
||||
Title: b.Title, Contour: b.Contour, SubjectCN: b.Info.SubjectCN, IssuerCN: b.Info.IssuerCN, Serial: b.Info.Serial,
|
||||
ValidFrom: b.Info.NotBefore.Format("02.01.2006"), ValidTo: b.Info.NotAfter.Format("02.01.2006"), bundleIdx: i,
|
||||
}
|
||||
_, present := leaf[normSerial(b.Info.Serial)]
|
||||
caPresent := ca[strings.TrimSpace(b.Info.IssuerCN)]
|
||||
switch {
|
||||
case now.After(b.Info.NotAfter):
|
||||
row.Kind, row.Status = "expired", "истёк "+row.ValidTo+" — пропущен"
|
||||
case present:
|
||||
row.Kind, row.Status = "ok", "в справочнике ИШ"
|
||||
case !caPresent:
|
||||
row.Kind, row.Status = "skip", "не для этого контура — в справочнике нет УЦ «"+b.Info.IssuerCN+"»"
|
||||
default:
|
||||
row.Kind, row.Status, row.Action = "add", "будет добавлен в справочник", "add"
|
||||
}
|
||||
// замена старого сертификата
|
||||
if b.ReplacesSerial != "" && row.Kind != "skip" && row.Kind != "expired" {
|
||||
if old, ok := leaf[normSerial(b.ReplacesSerial)]; ok {
|
||||
title := b.ReplacesTitle
|
||||
if title == "" {
|
||||
title = old.SubjectCN
|
||||
}
|
||||
after, perr := time.Parse(time.RFC3339, b.RemoveReplacedAfter)
|
||||
switch {
|
||||
case perr != nil || !now.Before(after):
|
||||
row.RemoveSerial = old.Serial
|
||||
row.RemoveNote = "старый сертификат «" + title + "» будет удалён"
|
||||
if perr == nil {
|
||||
row.RemoveNote += " (срок " + after.Local().Format("02.01.2006 15:04") + " наступил)"
|
||||
}
|
||||
default:
|
||||
row.RemoveNote = "старый «" + title + "» остаётся до " + after.Local().Format("02.01.2006 15:04") + ", затем удалится автоматически"
|
||||
}
|
||||
}
|
||||
}
|
||||
rows = append(rows, row)
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
func nsdPending(rows []NSDCertRow) int {
|
||||
n := 0
|
||||
for _, r := range rows {
|
||||
if r.Action == "add" {
|
||||
n++
|
||||
}
|
||||
if r.RemoveSerial != "" {
|
||||
n++
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// ---- проверка и применение ----
|
||||
|
||||
func nsdSaveState(rc *RuntimeConfig, profile string, rows []NSDCertRow, store []NSDStoreCert, logText string, applied bool) {
|
||||
s := rc.Snapshot().NSDCerts
|
||||
s.LastRun = time.Now()
|
||||
s.LastLog = logText
|
||||
if profile != "" {
|
||||
s.LastProfile = profile
|
||||
}
|
||||
if rows != nil {
|
||||
s.LastRows = rows
|
||||
}
|
||||
if store != nil {
|
||||
s.LastStore = store
|
||||
}
|
||||
if applied {
|
||||
s.LastApplied = time.Now()
|
||||
}
|
||||
if err := rc.UpdateNSDCerts(s); err != nil {
|
||||
log.Printf("nsdcerts: сохранение состояния: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// nsdCertsCheck — сверка без изменений.
|
||||
func nsdCertsCheck(ctx context.Context, rc *RuntimeConfig) ([]NSDCertRow, error) {
|
||||
profile, store, err := workerCertsList(ctx)
|
||||
if err != nil {
|
||||
nsdSaveState(rc, "", nil, nil, "сверка не выполнена: "+err.Error(), false)
|
||||
return nil, err
|
||||
}
|
||||
bundle, warns := loadNSDCertBundle()
|
||||
rows := nsdCertsPlan(bundle, store, time.Now())
|
||||
var sb strings.Builder
|
||||
fmt.Fprintf(&sb, "профиль %s: в справочнике %d записей, в комплекте %d сертификатов, ожидает изменений: %d", profile, len(store), len(bundle), nsdPending(rows))
|
||||
for _, w := range warns {
|
||||
sb.WriteString("\n⚠ " + w)
|
||||
}
|
||||
nsdSaveState(rc, profile, rows, store, sb.String(), false)
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
// nsdCertsApply — сверка и применение изменений (если они есть).
|
||||
func nsdCertsApply(ctx context.Context, rc *RuntimeConfig, trigger string) (string, bool, error) {
|
||||
profile, store, err := workerCertsList(ctx)
|
||||
if err != nil {
|
||||
nsdSaveState(rc, "", nil, nil, "сверка не выполнена: "+err.Error(), false)
|
||||
return "", false, err
|
||||
}
|
||||
bundle, warns := loadNSDCertBundle()
|
||||
now := time.Now()
|
||||
rows := nsdCertsPlan(bundle, store, now)
|
||||
var adds []nsdBundleCert
|
||||
var removes []string
|
||||
var what []string
|
||||
for _, r := range rows {
|
||||
if r.Action == "add" {
|
||||
adds = append(adds, bundle[r.bundleIdx])
|
||||
what = append(what, "добавить «"+r.Title+"»")
|
||||
}
|
||||
if r.RemoveSerial != "" {
|
||||
removes = append(removes, r.RemoveSerial)
|
||||
what = append(what, r.RemoveNote)
|
||||
}
|
||||
}
|
||||
if len(adds) == 0 && len(removes) == 0 {
|
||||
msg := fmt.Sprintf("профиль %s: изменений не требуется (в справочнике %d записей)", profile, len(store))
|
||||
for _, w := range warns {
|
||||
msg += "\n⚠ " + w
|
||||
}
|
||||
nsdSaveState(rc, profile, rows, store, msg, false)
|
||||
return msg, false, nil
|
||||
}
|
||||
|
||||
dir := filepath.Join(nsdCertsApplyDir, now.Format("20060102-150405"))
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return "", false, fmt.Errorf("staging: %w", err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
for _, a := range adds {
|
||||
if err := os.WriteFile(filepath.Join(dir, normSerial(a.Info.Serial)+".cer"), a.DER, 0o644); err != nil {
|
||||
return "", false, fmt.Errorf("staging: %w", err)
|
||||
}
|
||||
}
|
||||
res, err := runKeymediaOp(ctx, "certs-apply", map[string]string{"staging": dir, "extra": strings.Join(removes, ",")})
|
||||
if err != nil {
|
||||
msg := "НЕ ПРИМЕНЕНО (" + trigger + "): " + err.Error() + "\nплан: " + strings.Join(what, "; ")
|
||||
nsdSaveState(rc, profile, rows, store, msg, false)
|
||||
_ = rc.AddNews(NewsItem{At: time.Now(), Kind: "system", Title: "Сертификаты НРД: не удалось обновить справочник ИШ",
|
||||
Body: "Профиль " + profile + ". " + err.Error() + ". Справочник откачен из бэкапа, ИШ работает на прежнем наборе. План: " + strings.Join(what, "; ") + ". Проверьте носитель ключа и повторите в Настройки → Криптография."})
|
||||
return "", false, err
|
||||
}
|
||||
newStore := nsdStoreFromResult(res)
|
||||
rows2 := nsdCertsPlan(bundle, newStore, now)
|
||||
// контроль результата
|
||||
var problems []string
|
||||
for _, r := range rows2 {
|
||||
if r.Action == "add" {
|
||||
problems = append(problems, "«"+r.Title+"» так и не появился")
|
||||
}
|
||||
if r.RemoveSerial != "" {
|
||||
problems = append(problems, "старый "+r.RemoveSerial+" всё ещё в справочнике")
|
||||
}
|
||||
}
|
||||
igate, _ := res["igate"].(string)
|
||||
backup, _ := res["backup"].(string)
|
||||
msg := fmt.Sprintf("ПРИМЕНЕНО (%s) в %s: %s. ИШ перезапущен (%s), бэкап %s", trigger, now.Format("02.01.2006 15:04"), strings.Join(what, "; "), igate, backup)
|
||||
if len(problems) > 0 {
|
||||
msg += "\n⚠ " + strings.Join(problems, "; ")
|
||||
}
|
||||
for _, w := range warns {
|
||||
msg += "\n⚠ " + w
|
||||
}
|
||||
nsdSaveState(rc, profile, rows2, newStore, msg, true)
|
||||
_ = rc.AddNews(NewsItem{At: time.Now(), Kind: "system", Title: "Сертификаты НРД обновлены в справочнике ИШ",
|
||||
Body: "Профиль " + profile + ": " + strings.Join(what, "; ") + ". Справочник пересобран, подпись проверена, ИШ перезапущен. Бэкап: " + backup + "."})
|
||||
log.Printf("nsdcerts: %s", strings.ReplaceAll(msg, "\n", " | "))
|
||||
return msg, true, nil
|
||||
}
|
||||
|
||||
// nsdCertsTick — один проход планировщика.
|
||||
func nsdCertsTick(ctx context.Context, rc *RuntimeConfig) {
|
||||
s := rc.Snapshot()
|
||||
if s.NSD.IGWBaseURL == "" {
|
||||
return // mock-режим, реального ИШ нет
|
||||
}
|
||||
if st, err := os.Stat(keymediaReqDir); err != nil || !st.IsDir() {
|
||||
return // root-воркер не установлен
|
||||
}
|
||||
tctx, cancel := context.WithTimeout(ctx, 4*time.Minute)
|
||||
defer cancel()
|
||||
if s.NSDCerts.Manual {
|
||||
if rows, err := nsdCertsCheck(tctx, rc); err == nil && nsdPending(rows) > 0 {
|
||||
log.Printf("nsdcerts: ожидает изменений %d, автоприменение выключено", nsdPending(rows))
|
||||
}
|
||||
return
|
||||
}
|
||||
if _, changed, err := nsdCertsApply(tctx, rc, "авто"); err != nil {
|
||||
log.Printf("nsdcerts: авто: %v", err)
|
||||
} else if !changed {
|
||||
log.Printf("nsdcerts: авто: изменений не требуется")
|
||||
}
|
||||
}
|
||||
|
||||
// StartNSDCertsSync — при старте (через 90 с, чтобы ИШ/воркер поднялись) и
|
||||
// далее каждые 6 ч. Обновление бинаря bj → перезапуск → новый комплект
|
||||
// применится сам.
|
||||
func StartNSDCertsSync(rc *RuntimeConfig) func() {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
go func() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.After(nsdCertsStartWait):
|
||||
}
|
||||
for {
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Printf("nsdcerts: panic: %v", r)
|
||||
}
|
||||
}()
|
||||
nsdCertsTick(ctx, rc)
|
||||
}()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.After(nsdCertsInterval):
|
||||
}
|
||||
}
|
||||
}()
|
||||
return cancel
|
||||
}
|
||||
|
||||
// ---- UI ----
|
||||
|
||||
// NSDCertsView — данные карточки «Сертификаты НРД в справочнике ИШ».
|
||||
type NSDCertsView struct {
|
||||
Profile string
|
||||
Rows []NSDCertRow
|
||||
Store []NSDStoreCert
|
||||
LastRun time.Time
|
||||
LastLog string
|
||||
Manual bool
|
||||
Pending int
|
||||
Bundle int
|
||||
}
|
||||
|
||||
func nsdCertsView(s Settings) NSDCertsView {
|
||||
v := NSDCertsView{Profile: s.NSDCerts.LastProfile, Rows: s.NSDCerts.LastRows, Store: s.NSDCerts.LastStore,
|
||||
LastRun: s.NSDCerts.LastRun, LastLog: s.NSDCerts.LastLog, Manual: s.NSDCerts.Manual}
|
||||
bundle, _ := loadNSDCertBundle()
|
||||
v.Bundle = len(bundle)
|
||||
if len(v.Rows) == 0 {
|
||||
for _, b := range bundle {
|
||||
v.Rows = append(v.Rows, NSDCertRow{Title: b.Title, Contour: b.Contour, SubjectCN: b.Info.SubjectCN, IssuerCN: b.Info.IssuerCN, Serial: b.Info.Serial,
|
||||
ValidFrom: b.Info.NotBefore.Format("02.01.2006"), ValidTo: b.Info.NotAfter.Format("02.01.2006"), Kind: "unknown", Status: "ещё не сверялось"})
|
||||
}
|
||||
}
|
||||
v.Pending = nsdPending(v.Rows)
|
||||
return v
|
||||
}
|
||||
|
||||
func (h *setupHandlers) saveNSDCerts(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "method", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
s := h.rc.Snapshot().NSDCerts
|
||||
s.Manual = r.FormValue("auto") == ""
|
||||
if err := h.rc.UpdateNSDCerts(s); err != nil {
|
||||
setupFlash(w, r, "Сертификаты НРД: ошибка сохранения: "+err.Error())
|
||||
return
|
||||
}
|
||||
if s.Manual {
|
||||
setupFlash(w, r, "Сертификаты НРД: автоприменение выключено — только по кнопке «Применить сейчас».")
|
||||
} else {
|
||||
setupFlash(w, r, "Сертификаты НРД: автоприменение включено (при старте и каждые 6 ч).")
|
||||
}
|
||||
}
|
||||
|
||||
func (h *setupHandlers) checkNSDCerts(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "method", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Minute)
|
||||
defer cancel()
|
||||
rows, err := nsdCertsCheck(ctx, h.rc)
|
||||
if err != nil {
|
||||
setupFlash(w, r, "Сертификаты НРД: сверка не выполнена — "+err.Error())
|
||||
return
|
||||
}
|
||||
if n := nsdPending(rows); n > 0 {
|
||||
setupFlash(w, r, fmt.Sprintf("Сертификаты НРД: сверено, ожидает изменений: %d — нажмите «Применить сейчас» или дождитесь автоприменения.", n))
|
||||
return
|
||||
}
|
||||
setupFlash(w, r, "Сертификаты НРД: сверено, справочник ИШ актуален.")
|
||||
}
|
||||
|
||||
func (h *setupHandlers) applyNSDCerts(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "method", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 4*time.Minute)
|
||||
defer cancel()
|
||||
msg, _, err := nsdCertsApply(ctx, h.rc, "вручную")
|
||||
if err != nil {
|
||||
setupFlash(w, r, "Сертификаты НРД: НЕ применено — "+err.Error())
|
||||
return
|
||||
}
|
||||
if len(msg) > 600 {
|
||||
msg = msg[:600] + "…"
|
||||
}
|
||||
setupFlash(w, r, "Сертификаты НРД: "+strings.ReplaceAll(msg, "\n", " "))
|
||||
}
|
||||
Reference in New Issue
Block a user