Files
Bridge-and-Join-s/internal/lkgateway/keywizard.go
T

840 lines
34 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 (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
"time"
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/cryptocli"
)
// urlQ — экранирование строки для query-параметра flash.
func urlQ(s string) string { return url.QueryEscape(s) }
// Пошаговый мастер установки ключа Валидаты на съёмный носитель (USB keymedia
// ИШ). Реализует то, что просил пользователь: загрузил архив + пароль →
// распаковка → запись на флешку → формирование справочника сертификатов →
// проверка Валидаты → «Готово» → можно слать тестовый документ.
//
// Привилегированные операции с флешкой (бэкап, remount rw, запись, перезапуск
// VDCrySvc) делает помощник /usr/local/sbin/bj-keymedia-install через узкий
// sudoers (bj-server работает под непривилегированным bj).
// keyWizardStep — один шаг мастера.
type keyWizardStep struct {
Title string
Status string // pending | active | ok | error
Detail string
}
// keyWizardState — состояние одного прогона мастера (в памяти, один активный).
type keyWizardState struct {
mu sync.Mutex
StagingID string // id распаковки в /var/lib/bj/media/iso/<id>
VDK string // имя файла ключа
Profile string // имя установленного профиля на носителе
Backup string // путь бэкапа
Steps []keyWizardStep // 1..5
Done bool // все шаги пройдены
Flash string
// ключ взят с флешки (а не из архива): на ту же флешку и под тем же именем профиля
MediaDevice, MediaProfile string
}
func newKeyWizardState() *keyWizardState {
return &keyWizardState{Steps: defaultKeySteps()}
}
// reset обнуляет поля прогона, НЕ трогая мьютекс (вызывать под Lock).
func (s *keyWizardState) reset() {
s.StagingID = ""
s.VDK = ""
s.Profile = ""
s.Backup = ""
s.Steps = defaultKeySteps()
s.Done = false
s.Flash = ""
s.MediaDevice, s.MediaProfile = "", ""
}
func defaultKeySteps() []keyWizardStep {
last := "Готово — можно проверить подключения"
if devMode(globalRC) {
last = "Готово — можно отправлять тестовый документ"
}
return []keyWizardStep{
{Title: "Загрузка архива и распаковка", Status: "pending"},
{Title: "Запись ключа на новую флешку (копия прежнего содержимого — на сервере)", Status: "pending"},
{Title: "Формирование справочника сертификатов (CRL)", Status: "pending"},
{Title: "Перезапуск и проверка ИШ", Status: "pending"},
{Title: last, Status: "pending"},
}
}
func (s *keyWizardState) set(i int, status, detail string) {
if i >= 0 && i < len(s.Steps) {
s.Steps[i].Status = status
if detail != "" {
s.Steps[i].Detail = detail
}
}
}
// flashDrive — съёмный носитель (USB), обнаруженный в системе.
type flashDrive struct {
Device string // /dev/sdb1
Size string // 1,9G
Label string
FSType string
Mountpoint string // пусто если не смонтирован
Model string // USB2FlashStorage
IsKeymedia bool // смонтирован как текущий ключевой носитель ИШ
}
// keyWizardData — данные шаблона admin_keywizard.html.
type keyWizardData struct {
page
State *keyWizardState
Drives []flashDrive
SuggestContour string // контур по сертификату в архиве: test3 (издатель «Тестовый УЦ») | prod | "" (не понятно)
SuggestIssuer string
WantContour string // контур, для которого открыли установку (кнопка «Установить ключ» у канала)
InWizard bool // блок — на шаге мастера: формы возвращают туда же
}
// keyWizView — данные блока установки ключа (want — контур, у которого нажали «Установить ключ»).
func (h *setupHandlers) keyWizView(want string, inWizard bool) *keyWizardData {
if h.keyWiz == nil { // обработчики без мастера ключа (тесты)
h.keyWiz = newKeyWizardState()
}
h.keyWiz.mu.Lock()
st := h.keyWiz
h.keyWiz.mu.Unlock()
d := &keyWizardData{State: st, InWizard: inWizard}
if want == "test3" || want == "prod" {
d.WantContour = want
}
if !st.Done { // флешки: в начале — откуда взять ключ, после загрузки — куда записать
d.Drives = listFlashDrives()
}
if st.StagingID != "" && !st.Done {
d.SuggestContour, d.SuggestIssuer = keyArchiveContour(filepath.Join("/var/lib/bj/media/iso", st.StagingID))
}
return d
}
// keyWizBack — куда вернуться после действия: на шаг мастера (ret=wizard) или на отдельную страницу.
func keyWizBack(r *http.Request) string {
u := "/admin/setup/keywizard?"
if r.FormValue("ret") == "wizard" {
u = "/admin/wizard?step=8&"
}
if c := r.FormValue("want"); c == "test3" || c == "prod" {
u += "contour=" + c + "&"
}
return u
}
// keyArchiveContour — контур ключа по сертификатам в распакованном архиве (не самоподписанным — это УЦ):
// издатель «Тестовый УЦ» — TEST3, иначе боевой. ГОСТ-сертификаты разбираются по TBS (parseNSDCert).
func keyArchiveContour(dir string) (contour, issuer string) {
_ = filepath.Walk(dir, func(p string, info os.FileInfo, err error) error {
if err != nil || info.IsDir() || info.Size() > 64<<10 || contour != "" {
return nil
}
switch strings.ToLower(filepath.Ext(p)) {
case ".cer", ".crt", ".pem":
default:
return nil
}
raw, err := os.ReadFile(p)
if err != nil {
return nil
}
ci, _, err := parseNSDCert(raw)
if err != nil || ci.SelfSigned || ci.IssuerCN == "" {
return nil
}
issuer = ci.IssuerCN
if strings.Contains(strings.ToLower(ci.IssuerCN), "тестов") {
contour = "test3"
} else {
contour = "prod"
}
return nil
})
return contour, issuer
}
const keymediaMount = "/var/lib/igate/keymedia"
// listFlashDrives перечисляет съёмные (removable/hotplug) USB-носители с ФС —
// чтобы пользователь выбрал, на какую флешку писать ключ.
func listFlashDrives() []flashDrive {
out, err := exec.Command("lsblk", "-J", "-b", "-o",
"NAME,SIZE,LABEL,MOUNTPOINT,RM,HOTPLUG,TYPE,MODEL,FSTYPE,PATH").Output()
if err != nil {
return nil
}
var parsed struct {
Blockdevices []json.RawMessage `json:"blockdevices"`
}
if json.Unmarshal(out, &parsed) != nil {
return nil
}
var drives []flashDrive
var walk func(raw []byte, parentRemovable bool, parentModel string)
walk = func(raw []byte, parentRemovable bool, parentModel string) {
var d struct {
Name string `json:"name"`
Size int64 `json:"size"`
Label string `json:"label"`
Mountpoint string `json:"mountpoint"`
RM bool `json:"rm"`
Hotplug bool `json:"hotplug"`
Type string `json:"type"`
Model string `json:"model"`
FSType string `json:"fstype"`
Path string `json:"path"`
Children []json.RawMessage `json:"children"`
}
if json.Unmarshal(raw, &d) != nil {
return
}
removable := d.RM || d.Hotplug || parentRemovable
model := strings.TrimSpace(d.Model)
if model == "" {
model = parentModel
}
// Носитель с ФС — кандидат на запись.
if removable && (d.Type == "part" || d.Type == "disk") && d.FSType != "" {
drives = append(drives, flashDrive{
Device: d.Path,
Size: humanSize(d.Size),
Label: d.Label,
FSType: d.FSType,
Mountpoint: d.Mountpoint,
Model: model,
IsKeymedia: d.Mountpoint == keymediaMount,
})
}
for _, c := range d.Children {
walk(c, removable, model)
}
}
for _, b := range parsed.Blockdevices {
walk(b, false, "")
}
return drives
}
func humanSize(b int64) string {
switch {
case b >= 1<<30:
return fmt.Sprintf("%.1f ГБ", float64(b)/(1<<30))
case b >= 1<<20:
return fmt.Sprintf("%.0f МБ", float64(b)/(1<<20))
default:
return fmt.Sprintf("%d Б", b)
}
}
// registerKeyWizard вешает маршруты мастера установки ключа.
func (h *setupHandlers) registerKeyWizard(mux *http.ServeMux) {
if h.keyWiz == nil {
h.keyWiz = newKeyWizardState()
}
mux.HandleFunc("/admin/setup/keywizard", h.renderKeyWizard)
mux.HandleFunc("/admin/setup/keywizard/upload", h.keyWizardUpload)
mux.HandleFunc("/admin/setup/keywizard/install", h.keyWizardInstall)
mux.HandleFunc("/admin/setup/keywizard/reset", h.keyWizardReset)
mux.HandleFunc("/admin/setup/keywizard/from-media", h.keyWizardFromMedia)
}
func (h *setupHandlers) renderKeyWizard(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "method", http.StatusMethodNotAllowed)
return
}
h.keyWiz.mu.Lock()
h.keyWiz.Flash = r.URL.Query().Get("flash")
h.keyWiz.mu.Unlock()
d := h.keyWizView(r.URL.Query().Get("contour"), false)
d.page = nowPage("Установка ключа", "setup")
render(w, h.tpl.a.keyWizard, *d)
}
// keyWizardUseMedia — «использовать ключ с подготовленной флешки как есть»: на флешку ничего не пишется.
// Справочник для ИШ собирается из её копии на диске, флешка подключается к ИШ постоянно и только на чтение.
func (h *setupHandlers) keyWizardUseMedia(w http.ResponseWriter, r *http.Request, staging string) {
h.keyWiz.mu.Lock()
dev, prof := h.keyWiz.MediaDevice, h.keyWiz.MediaProfile
h.keyWiz.mu.Unlock()
found := false
for _, d := range listFlashDrives() {
found = found || d.Device == dev
}
if dev == "" || !found {
h.keyWizFlash(w, r, "Флешка с ключом не найдена — вставьте её и нажмите «Использовать ключ с флешки» ещё раз.")
return
}
contour := r.FormValue("contour")
if contour != "prod" {
contour = "test3"
}
ctx, cancel := context.WithTimeout(r.Context(), 3*time.Minute)
defer cancel()
res, err := runKeymediaOp(ctx, "use-media", map[string]string{"staging": staging, "profile": prof, "target_dev": dev, "channel": contourChannel(contour)})
h.keyWiz.mu.Lock()
if err != nil || res["ok"] != true {
msg := errStr(err)
if e, ok := res["error"].(string); ok {
msg = e
}
h.keyWiz.set(1, "error", "Ключ с флешки не подключён: "+msg)
h.keyWiz.mu.Unlock()
h.keyWizFlash(w, r, "Ключ с флешки не подключён: "+msg)
return
}
if p, ok := res["profile"].(string); ok {
h.keyWiz.Profile = p
}
rebuild, _ := res["rebuild"].(string)
igate, _ := res["igate"].(string)
h.keyWiz.set(1, "ok", "Флешка "+dev+" подключена к ИШ только для чтения — на неё ничего не записано")
if bad := keyInstallProblem(res); bad != "" {
h.keyWiz.set(2, "error", bad)
h.keyWiz.mu.Unlock()
h.keyWizFlash(w, r, "Ключ с флешки не подключён: "+bad)
return
}
h.keyWiz.set(2, "ok", fmt.Sprintf("Справочник для ИШ (%s); профиль «%s» %s ключ подписывает", rebuildRu(rebuild), h.keyWiz.Profile, igateRu(igate)))
h.keyWiz.set(3, "active", "")
h.keyWiz.mu.Unlock()
h.reactivateCryptoProfile(ctx)
ishOK, ishMsg := h.restartAndVerifyISH(ctx)
h.keyWiz.mu.Lock()
if !ishOK {
h.keyWiz.set(3, "error", "ИШ не подтвердил готовность: "+ishMsg)
h.keyWiz.mu.Unlock()
h.keyWizFlash(w, r, "Ключ подключён, но ИШ не готов: "+ishMsg)
return
}
h.keyWiz.set(3, "ok", "ИШ перезапущен и работает: "+ishMsg)
h.keyWiz.set(4, "ok", "Ключ с флешки подключён — проверьте подключения на странице «Статус»")
h.keyWiz.Done = true
h.keyWiz.mu.Unlock()
_ = os.RemoveAll(staging)
refreshCertStore(h.rc)
http.Redirect(w, r, keyWizBack(r)+"flash="+urlQ("Готово! Ключ с флешки подключён к ИШ как есть — флешка не менялась."), http.StatusSeeOther)
}
// keyWizardFromMedia — POST /admin/setup/keywizard/from-media: ключ уже на флешке (подготовленной bj раньше
// или с ключом и справочником от НРД). Флешка только читается: ключ и справочник копируются во временный
// каталог — дальше обычная установка на эту же флешку под тем же именем профиля (с резервной копией флешки).
func (h *setupHandlers) keyWizardFromMedia(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method", http.StatusMethodNotAllowed)
return
}
dev := strings.TrimSpace(r.FormValue("source_device"))
var drive *flashDrive
for _, d := range listFlashDrives() {
if d.Device == dev {
d := d
drive = &d
}
}
if drive == nil { // только обнаруженные съёмные: иначе root-воркер смонтировал бы что угодно из формы
h.keyWizFlash(w, r, "Флешка не найдена среди съёмных носителей — вставьте её и обновите страницу.")
return
}
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Minute)
defer cancel()
res, err := runKeymediaOp(ctx, "stage-media", map[string]string{"target_dev": drive.Device, "target_mnt": drive.Mountpoint})
if err == nil && res["ok"] != true {
err = fmt.Errorf("%v", res["error"])
}
if err != nil {
h.keyWizFlash(w, r, "Ключ с флешки не взят: "+err.Error())
return
}
id, _ := res["staging"].(string)
if !strings.HasPrefix(id, "media-") {
h.keyWizFlash(w, r, "Ключ с флешки не взят: установщик не вернул каталог")
return
}
vdk, _ := res["vdk"].(string)
prof, _ := res["profile"].(string)
spr, _ := res["spr"].(string)
h.keyWiz.mu.Lock()
h.keyWiz.reset()
h.keyWiz.StagingID, h.keyWiz.VDK = id, vdk
h.keyWiz.MediaDevice, h.keyWiz.MediaProfile = drive.Device, prof
// флешка уже подготовлена: используется как есть — этапы называются по-другому, записи на флешку нет
h.keyWiz.Steps[0].Title = "Поиск ключа на флешке"
h.keyWiz.Steps[1].Title = "Подключение флешки к шлюзу как есть — на неё ничего не записывается"
h.keyWiz.Steps[2].Title = "Справочник сертификатов для ИШ — на диске сервера"
h.keyWiz.set(0, "ok", fmt.Sprintf("Ключ найден на флешке %s: %s, справочник %s%s", fallback(drive.Model, drive.Device), vdk, spr, prefixed(", профиль ", prof)))
h.keyWiz.set(1, "active", "")
h.keyWiz.mu.Unlock()
http.Redirect(w, r, strings.TrimRight(keyWizBack(r), "?&"), http.StatusSeeOther)
}
func (h *setupHandlers) keyWizardReset(w http.ResponseWriter, r *http.Request) {
h.keyWiz.mu.Lock()
h.keyWiz.reset()
h.keyWiz.mu.Unlock()
http.Redirect(w, r, strings.TrimRight(keyWizBack(r), "?&"), http.StatusSeeOther)
}
// keyWizardUpload — шаг 1: приём .7z + пароль, распаковка, инспекция.
func (h *setupHandlers) keyWizardUpload(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method", http.StatusMethodNotAllowed)
return
}
if err := r.ParseMultipartForm(500 << 20); err != nil {
h.keyWizFlash(w, r, "Ошибка чтения формы: "+err.Error())
return
}
file, header, err := r.FormFile("archive")
if err != nil {
h.keyWizFlash(w, r, "Выберите файл архива (.7z/.zip)")
return
}
defer file.Close()
lower := strings.ToLower(header.Filename)
if !(strings.HasSuffix(lower, ".7z") || strings.HasSuffix(lower, ".zip")) {
h.keyWizFlash(w, r, "Архив должен быть .7z или .zip")
return
}
password := r.FormValue("password")
// Не копим staging: чистим прошлые распаковки/архивы перед новой загрузкой.
sweepStaging()
isoDir := "/var/lib/bj/iso"
if err := os.MkdirAll(isoDir, 0o755); err != nil {
h.keyWizFlash(w, r, "Не удалось создать "+isoDir+": "+err.Error())
return
}
dst := filepath.Join(isoDir, time.Now().UTC().Format("20060102-150405-")+filepath.Base(header.Filename))
out, err := os.Create(dst)
if err != nil {
h.keyWizFlash(w, r, "Запись архива: "+err.Error())
return
}
if _, err := io.Copy(out, file); err != nil {
out.Close()
_ = os.Remove(dst)
h.keyWizFlash(w, r, "Запись архива: "+err.Error())
return
}
out.Close()
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Minute)
defer cancel()
m, err := ExtractISO(ctx, dst, password)
if err != nil {
h.keyWizFlash(w, r, "Распаковка не удалась (проверьте пароль): "+err.Error())
return
}
h.keyWiz.mu.Lock()
h.keyWiz.reset()
h.keyWiz.StagingID = m.ID
// Инспекция через помощник (читает staging, находит .vdk/gdbm/pse).
staging := filepath.Join("/var/lib/bj/media/iso", m.ID)
insp, ierr := runKeymediaHelper(ctx, "inspect", staging, "", "", "")
if ierr == nil && insp["ok"] == true {
if v, ok := insp["vdk"].(string); ok {
h.keyWiz.VDK = v
}
}
detail := fmt.Sprintf("Ключ: %s · справочник сертификатов: %s",
fallback(h.keyWiz.VDK, "—"), yesNo(insp["has_gdbm"]))
h.keyWiz.set(0, "ok", detail)
h.keyWiz.set(1, "active", "")
h.keyWiz.mu.Unlock()
http.Redirect(w, r, strings.TrimRight(keyWizBack(r), "?&"), http.StatusSeeOther)
}
// keyWizardInstall — шаги 2-5: запись на флешку, справочник, проверка, готово.
func (h *setupHandlers) keyWizardInstall(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method", http.StatusMethodNotAllowed)
return
}
h.keyWiz.mu.Lock()
id := h.keyWiz.StagingID
h.keyWiz.mu.Unlock()
if id == "" {
h.keyWizFlash(w, r, "Сначала загрузите архив (шаг 1)")
return
}
staging := filepath.Join("/var/lib/bj/media/iso", id)
if r.FormValue("as_is") == "1" {
h.keyWizardUseMedia(w, r, staging)
return
}
// Выбор флешки и имя профиля из формы.
profileName := strings.TrimSpace(r.FormValue("profile_name"))
targetDev := strings.TrimSpace(r.FormValue("target_device"))
targetMnt := ""
if targetDev != "" {
found := false
for _, d := range listFlashDrives() {
if d.Device == targetDev {
targetMnt, found = d.Mountpoint, true
break
}
}
// Устройство должно быть из списка обнаруженных съёмных: иначе root-воркер
// смонтировал бы произвольный блочный девайс/путь, переданный из формы.
if !found {
h.keyWizFlash(w, r, "Выбранное устройство не найдено среди съёмных носителей — обновите список и повторите.")
return
}
}
ctx, cancel := context.WithTimeout(r.Context(), 3*time.Minute)
defer cancel()
// Шаг 2-3: запись ключа на флешку + справочник + CRL (привилегированный воркер).
// контур ключа: профиль Валидаты привязывается только к каналу ИШ этого контура (у TEST3 и боевого — свои ключи)
contour := r.FormValue("contour")
if contour != "prod" {
contour = "test3"
}
res, err := runKeymediaOp(ctx, "install", map[string]string{"staging": staging, "profile": profileName,
"target_dev": targetDev, "target_mnt": targetMnt, "channel": contourChannel(contour)})
h.keyWiz.mu.Lock()
if err != nil || res["ok"] != true {
msg := errStr(err)
if e, ok := res["error"].(string); ok {
msg = e
}
h.keyWiz.set(1, "error", "Запись на флешку не удалась: "+msg)
h.keyWiz.mu.Unlock()
h.keyWizFlash(w, r, "Установка прервана: "+msg)
return
}
if p, ok := res["profile"].(string); ok {
h.keyWiz.Profile = p
}
if b, ok := res["backup"].(string); ok {
h.keyWiz.Backup = b
}
tgt, _ := res["target"].(string)
spr, _ := res["spr"].(string)
h.keyWiz.set(1, "ok", fmt.Sprintf("Профиль «%s» записан на %s. Бэкап: %s", h.keyWiz.Profile, fallback(tgt, "носитель"), h.keyWiz.Backup))
rebuild, _ := res["rebuild"].(string)
igate, _ := res["igate"].(string)
igp, _ := res["igate_profile_dir"].(string)
if bad := keyInstallProblem(res); bad != "" {
h.keyWiz.set(2, "error", bad)
h.keyWiz.mu.Unlock()
h.keyWizFlash(w, r, "Ключ записан на флешку, но не подключён: "+bad)
return
}
h.keyWiz.set(2, "ok", fmt.Sprintf("Справочник «%s» пересобран (%s); профиль ИШ %s %s",
fallback(spr, "—"), rebuildRu(rebuild), igateRu(igate), igp))
h.keyWiz.set(3, "active", "")
h.keyWiz.mu.Unlock()
// Шаг 4: перезапуск и проверка ИШ. Перезапуск VDCrySvc мог сбросить
// активацию серверного профиля bj-crypto — восстанавливаем. Затем
// перезапускаем ИШ и проверяем engine/state: ИШ поднялся с новым ключом.
h.reactivateCryptoProfile(ctx)
ishOK, ishMsg := h.restartAndVerifyISH(ctx)
h.keyWiz.mu.Lock()
if !ishOK {
h.keyWiz.set(3, "error", "ИШ не подтвердил готовность: "+ishMsg)
h.keyWiz.mu.Unlock()
h.keyWizFlash(w, r, "Ключ записан, но ИШ не готов: "+ishMsg)
return
}
h.keyWiz.set(3, "ok", "ИШ перезапущен и работает: "+ishMsg)
dev := devMode(h.rc)
if dev {
h.keyWiz.set(4, "ok", "Теперь подпишите новым ключом — отправьте тестовый документ роботу НРД ниже")
} else {
h.keyWiz.set(4, "ok", "Ключ установлен — проверьте подключения на странице «Статус»")
}
h.keyWiz.Done = true
h.keyWiz.mu.Unlock()
// Ключ уже на флешке и в ИШ — распаковка staging больше не нужна.
_ = os.RemoveAll(staging)
refreshCertStore(h.rc) // шаг «Сертификаты» и сроки — сразу по новому ключу, а не через 6 часов
final := "Готово! Ключ на флешке, справочник сформирован, ИШ перезапущен и работает."
if dev {
final += " Финальная проверка — тестовым документом роботу."
}
http.Redirect(w, r, keyWizBack(r)+"flash="+urlQ(final), http.StatusSeeOther)
}
// restartAndVerifyISH перезапускает Интеграционный шлюз и проверяет, что он
// поднялся (engine/state). Возвращает (ok, сообщение).
func (h *setupHandlers) restartAndVerifyISH(ctx context.Context) (bool, string) {
// Перезапуск igate через привилегированный воркер (bj не sudoer).
res, err := runKeymediaHelper(ctx, "restart-ish", "/var/lib/bj/media/iso", "", "", "")
if err != nil || res["ok"] != true {
// restart-ish может быть не поддержан — не критично, проверим состояние.
_ = err
}
// Проверяем состояние ИШ через nsd-адаптер (engine/state).
deadline := time.Now().Add(40 * time.Second)
for time.Now().Before(deadline) {
if st := h.ishEngineState(ctx); st != "" {
return true, "engine " + st
}
select {
case <-ctx.Done():
return false, "таймаут"
case <-time.After(3 * time.Second):
}
}
return false, "ИШ не ответил на /api/admin/engine/state за 40 сек"
}
// ishEngineState запрашивает состояние движка ИШ; пусто если недоступен.
func (h *setupHandlers) ishEngineState(ctx context.Context) string {
s := h.rc.Snapshot()
base := s.NSD.IGWBaseURL
if base == "" {
return ""
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, strings.TrimRight(base, "/")+"/api/admin/engine/state", nil)
if err != nil {
return ""
}
cl := &http.Client{Timeout: 5 * time.Second}
resp, err := cl.Do(req)
if err != nil {
return ""
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return ""
}
b, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
return strings.TrimSpace(string(b))
}
func crlRu(s string) string {
switch s {
case "updated":
return "обновлены из точек распространения"
case "failed":
return "не удалось обновить (проверьте сеть/CDP)"
case "skip":
return "пропущено"
default:
return s
}
}
// rebuildRu — статус пересборки справочника сертификатов на флешке.
// keyInstallProblem — что не так в итоге установщика ключа (пусто — ключ подключён и подписывает).
// Раньше мастер ставил галочки по порядку, даже если справочник не собрался и профиль ИШ не создан.
func keyInstallProblem(res map[string]any) string {
rebuild, _ := res["rebuild"].(string)
igate, _ := res["igate"].(string)
check, _ := res["keycheck"].(string)
switch {
case rebuild != "ok" && rebuild != "as-is":
return "справочник сертификатов не собран — " + rebuildRu(rebuild) + " Если не инициализирован датчик случайных чисел Валидаты (блок «Датчик случайных чисел» выше) — пересборка не подпишется: поводите мышью в поле до заполнения полосы и повторите."
case igate != "connected" && igate != "pending-channel":
return "профиль ИШ не сформирован — " + strings.TrimSuffix(igateRu(igate), ":")
case strings.HasPrefix(check, "fail"):
diag, _ := res["keydiag"].(string)
return "ключ не подписывает: " + strings.TrimPrefix(check, "fail: ") + prefixed(". Что видит шлюз — ", diag) + " Возможно, справочник сертификатов не от этого ключа — ключ (.vdk) и справочник (local.pse/local.gdbm) должны быть из одного архива НРД."
}
return ""
}
func rebuildRu(s string) string {
switch {
case s == "ok":
return "совместимый формат, сертификаты извлечены из архива"
case s == "as-is":
return "справочник с флешки уже совместимый — взят как есть"
case s == "skip":
return "пропущено"
case strings.HasPrefix(s, "fail"):
return "ошибка пересборки (" + strings.TrimPrefix(s, "fail: ") + ")"
default:
return s
}
}
// igateRu — статус формирования профиля для ИШ.
func igateRu(s string) string {
switch s {
case "connected":
return "сформирован, зарегистрирован и подключён к ИШ (cryptography.profile):"
case "pending-channel":
return "сформирован и зарегистрирован; канала этого контура в ИШ ещё нет — ключ подключится, когда вы настроите канал на шаге «Шлюз НРД»:"
case "ok":
return "сформирован и зарегистрирован (но не выбран в ИШ):"
case "skip":
return "не формировался"
case "ini-fail":
return "сформирован, но регистрация в zpki.ini не удалась:"
default:
return s
}
}
// reactivateCryptoProfile повторно активирует текущий серверный профиль
// bj-crypto (после перезапуска VDCrySvc активация в сайдкаре сбрасывается).
// Best-effort: возвращает true при успехе.
func (h *setupHandlers) reactivateCryptoProfile(ctx context.Context) bool {
s := h.rc.Snapshot()
if s.Crypto.Profile == "" {
return false
}
cli := cryptocli.New(cryptocli.Config{
Provider: cryptocli.Provider(s.Crypto.Provider),
SocketPath: s.Crypto.SocketPath,
})
defer cli.Close()
res, err := cli.Activate(ctx, s.Crypto.Profile)
return err == nil && res.OK
}
// vdcrysvcActive проверяет, что демон Валидаты (vdmkdev) запущен.
func vdcrysvcActive() bool {
out, _ := exec.Command("systemctl", "is-active", "vdmkdev.service").Output()
return strings.TrimSpace(string(out)) == "active"
}
func boolRu(b bool, yes, no string) string {
if b {
return yes
}
return no
}
// sweepStaging удаляет прошлые распаковки (/var/lib/bj/media/iso/*) и архивы
// (/var/lib/bj/iso/*.7z|zip), чтобы staging не копился. Вызывается перед новой
// загрузкой — текущая распаковка создаётся уже после очистки и не страдает.
func sweepStaging() {
if entries, err := os.ReadDir("/var/lib/bj/media/iso"); err == nil {
for _, e := range entries {
_ = os.RemoveAll(filepath.Join("/var/lib/bj/media/iso", e.Name()))
}
}
for _, pat := range []string{"/var/lib/bj/iso/*.7z", "/var/lib/bj/iso/*.zip"} {
if files, err := filepath.Glob(pat); err == nil {
for _, f := range files {
_ = os.Remove(f)
}
}
}
}
const keymediaReqDir = "/var/lib/bj/keymedia-requests"
// runKeymediaHelper передаёт запрос привилегированному воркеру через файловый
// обмен: bj-server (в песочнице) пишет <id>.req, root-сервис bj-keymedia
// (host namespace, триггерится bj-keymedia.path) выполняет операцию с флешкой
// и пишет <id>.res. bj-server опрашивает результат. Так привилегированная
// работа идёт вне mount-namespace песочницы, где доступно перемонтирование USB.
func runKeymediaHelper(ctx context.Context, action, staging, profile, targetDev, targetMnt string) (map[string]any, error) {
return runKeymediaOp(ctx, action, map[string]string{"staging": staging, "profile": profile, "target_dev": targetDev, "target_mnt": targetMnt})
}
// runKeymediaOp — общий вход к root-воркеру: action + произвольные поля
// запроса (staging, profile, target_dev, target_mnt, extra). Используется и
// мастером ключа, и автообновлением сертификатов НРД (nsdcerts.go).
func runKeymediaOp(ctx context.Context, action string, fields map[string]string) (map[string]any, error) {
id := fmt.Sprintf("%s-%d", action, time.Now().UnixNano())
reqPath := filepath.Join(keymediaReqDir, id+".req")
resPath := filepath.Join(keymediaReqDir, id+".res")
defer os.Remove(resPath)
req := map[string]string{"action": action}
for k, v := range fields {
req[k] = v
}
reqBody, _ := json.Marshal(req)
// Пишем атомарно (tmp → rename), чтобы .path не подхватил полупустой файл.
tmp := reqPath + ".tmp"
if err := os.WriteFile(tmp, reqBody, 0o660); err != nil {
return nil, fmt.Errorf("запись запроса: %w", err)
}
if err := os.Rename(tmp, reqPath); err != nil {
return nil, fmt.Errorf("публикация запроса: %w", err)
}
// Опрос результата.
ticker := time.NewTicker(200 * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return nil, fmt.Errorf("таймаут ожидания воркера установки ключа")
case <-ticker.C:
b, err := os.ReadFile(resPath)
if err != nil {
continue // ещё не готово
}
res := map[string]any{}
// systemd/скрипты могут напечатать предупреждение перед JSON — берём с первой «{».
if i := bytes.IndexByte(b, '{'); i > 0 {
b = b[i:]
}
if jerr := json.Unmarshal(b, &res); jerr != nil {
return nil, fmt.Errorf("разбор ответа воркера: %v (%s)", jerr, strings.TrimSpace(string(b)))
}
if res["ok"] != true {
msg, _ := res["error"].(string)
return res, fmt.Errorf("воркер: %s", fallback(msg, "ошибка"))
}
return res, nil
}
}
}
func (h *setupHandlers) keyWizFlash(w http.ResponseWriter, r *http.Request, msg string) {
http.Redirect(w, r, keyWizBack(r)+"flash="+urlQ(msg), http.StatusSeeOther)
}
func fallback(s, def string) string {
if s == "" {
return def
}
return s
}
func yesNo(v any) string {
if b, ok := v.(bool); ok && b {
return "да"
}
return "нет"
}
func errStr(err error) string {
if err == nil {
return "неизвестная ошибка"
}
return err.Error()
}