4332e3309b
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
610 lines
22 KiB
Go
610 lines
22 KiB
Go
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", map[string]string{"channel": contourChannel(contourOf(currentNSDProfile()))})
|
||
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, ","), "channel": contourChannel(contourOf(currentNSDProfile()))})
|
||
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", " "))
|
||
}
|