Files
Bridge-and-Join-s/internal/lkgateway/igate.go
T
zuevav 10305a095e Первичная настройка, повторная проверка: копии без ключевых носителей СКЗИ (--one-file-system, без keymedia), без отпечатка железа и списка компонентов; восстановление на новый сервер — пароль базы bj из копии, база и пользователь ИШ; ключ для контура без канала запоминается и подключается при настройке канала; первый настроенный канал становится рабочим контуром; справочник сертификатов перечитывается после ключа и смены контура; шлюз НРД — шаг 7, ключ — шаг 8 (ключ ставится в профиль ИШ); подсказка контура ключа по сертификату; ИШ: параметры работающего шлюза не перезаписываются; без чужого кода отправителя и получателя по умолчанию; прокси для выхода в интернет; установка с образа без интернета; HTTPS — новый сертификат при смене адреса; bj-network — без cloud-init; проверка готовности: справочник участников, сетевые доступы, учётные записи, последний обмен с НРД
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
2026-10-05 17:33:10 +03:00

324 lines
13 KiB
Go

package lkgateway
// Интеграционный шлюз НРД (ИШ, служба igate) на этом сервере: состояние для мастера (шаг 7).
import (
"context"
"encoding/json"
"io"
"log"
"net/http"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"time"
)
const igateDefaultURL = "http://localhost:8090"
// igateInstalled — пакет ИШ стоит на этом сервере.
func igateInstalled() bool {
_, err := os.Stat("/opt/nsd/igate")
return err == nil
}
// igateState — отвечает ли ИШ и в каком состоянии его движок.
func igateState(ctx context.Context, base string) (bool, string) {
base = strings.TrimRight(pickStr(strings.TrimSpace(base), igateDefaultURL), "/")
cctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
req, _ := http.NewRequestWithContext(cctx, http.MethodGet, base+"/api/admin/engine/state", nil)
resp, err := http.DefaultClient.Do(req)
if err != nil {
if !igateInstalled() {
return false, "интеграционный шлюз на этом сервере не установлен"
}
out, _ := exec.CommandContext(ctx, "systemctl", "is-active", "igate").Output()
return false, "шлюз установлен, но не отвечает по адресу " + base + " (служба igate: " + pickStr(strings.TrimSpace(string(out)), "неизвестно") + ")"
}
defer resp.Body.Close()
body, _ := io.ReadAll(io.LimitReader(resp.Body, 256))
st := strings.Trim(strings.TrimSpace(string(body)), `"`)
if resp.StatusCode != http.StatusOK {
return false, "шлюз отвечает с ошибкой HTTP " + resp.Status
}
if st != "Running" {
return false, "шлюз отвечает, но его движок в состоянии «" + st + "» — нужен Running"
}
return true, "шлюз отвечает по адресу " + base + ", движок работает"
}
func registerIgate(mux *http.ServeMux, rc *RuntimeConfig) {
registerIgateAdmin(mux, rc)
mux.HandleFunc("/admin/setup/igate/state", func(w http.ResponseWriter, r *http.Request) {
ok, msg := igateState(r.Context(), rc.Snapshot().NSD.IGWBaseURL)
jsonOut(w, map[string]any{"ok": ok, "msg": msg, "installed": igateInstalled()})
})
}
// ---- контуры НРД: TEST3 и боевой ----
func currentNSDProfile() string {
if globalRC == nil {
return ""
}
return globalRC.Snapshot().NSD.Profile
}
// contourOf — контур по профилю bj: prod | guest | test3.
func contourOf(profile string) string {
switch {
case strings.HasPrefix(profile, "prod"):
return "prod"
case strings.HasPrefix(profile, "guest"):
return "guest"
}
return "test3"
}
// contourChannel — начало кода канала ИШ контура (канал = <код><депозитарный код>).
func contourChannel(contour string) string {
switch contour {
case "prod":
return "PROD"
case "guest":
return "GUEST"
}
return "TEST3"
}
// igateExchangeDir — папка обмена канала ИШ контура (у боевого — своя, чтобы входящие контуров не смешивались).
func igateExchangeDir(profile string) string {
if contourOf(profile) == "prod" {
return "/var/lib/igate/exchange-prod"
}
return "/var/lib/igate/exchange"
}
const igateUploadDir = "/var/lib/bj/igate-upload"
var igateVersionRe = regexp.MustCompile(`^\d+\.\d+\.\d+$`)
func registerIgateAdmin(mux *http.ServeMux, rc *RuntimeConfig) {
// состояние ИШ и каналов + что выбрано в bj
mux.HandleFunc("/admin/setup/igate/status", func(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
defer cancel()
res, err := runKeymediaOp(ctx, "igate-status", nil)
if err != nil {
jsonOut(w, map[string]any{"ok": false, "error": err.Error()})
return
}
s := rc.Snapshot()
res["active_contour"] = ""
if s.NSD.Profile != "" {
res["active_contour"] = contourOf(s.NSD.Profile)
}
res["active_profile"] = s.NSD.Profile
res["deponent"] = pickStr(s.NSD.DeponentCode, orgCode(s))
res["depo_db"] = s.Fancy.DepoDB()
res["depo_db_test"], res["depo_db_prod"] = s.NSD.DepoDBTest, s.NSD.DepoDBProd
jsonOut(w, res)
})
// установка или обновление ИШ: загруженный архив НРД или версия для скачивания с nsd.ru
mux.HandleFunc("/admin/setup/igate/install", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method", http.StatusMethodNotAllowed)
return
}
params := map[string]string{}
if f, hdr, err := r.FormFile("file"); err == nil {
defer f.Close()
name := strings.ToLower(hdr.Filename)
if !strings.HasSuffix(name, ".zip") && !strings.HasSuffix(name, ".deb") {
jsonOut(w, map[string]any{"ok": false, "error": "нужен дистрибутив ИШ от НРД: igate-…-amd64.zip (или .deb из него)"})
return
}
_ = os.MkdirAll(igateUploadDir, 0o750)
dst := filepath.Join(igateUploadDir, "igate"+filepath.Ext(name))
out, err := os.Create(dst)
if err != nil {
jsonOut(w, map[string]any{"ok": false, "error": err.Error()})
return
}
_, err = io.Copy(out, io.LimitReader(f, 600<<20))
out.Close()
if err != nil {
jsonOut(w, map[string]any{"ok": false, "error": "загрузка: " + err.Error()})
return
}
params["file"] = dst
} else if v := strings.TrimSpace(r.FormValue("version")); igateVersionRe.MatchString(v) {
params["version"] = v
} else {
jsonOut(w, map[string]any{"ok": false, "error": "загрузите архив ИШ или укажите версию вида 107.0.930"})
return
}
if u := rc.Snapshot().Proxy.url(); u != nil {
params["proxy"] = u.String() // скачивание с nsd.ru и пакеты — через прокси организации
}
extra, _ := json.Marshal(params)
ctx, cancel := context.WithTimeout(r.Context(), time.Minute)
defer cancel()
jsonOut(w, opResult(runKeymediaOp(ctx, "igate-install", map[string]string{"extra": string(extra)})))
})
mux.HandleFunc("/admin/setup/igate/install-status", func(w http.ResponseWriter, r *http.Request) {
raw, err := os.ReadFile("/var/lib/bj/igate-install-status.json")
if err != nil {
jsonOut(w, map[string]any{})
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Header().Set("Cache-Control", "no-store")
_, _ = w.Write(raw)
})
// канал контура в ИШ (адрес, отпечаток УЦ, папка обмена); ключ — мастером установки ключа
mux.HandleFunc("/admin/setup/igate/contour", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method", http.StatusMethodNotAllowed)
return
}
s := rc.Snapshot()
contour, crypto := r.FormValue("contour"), r.FormValue("crypto")
if contour != "prod" {
contour = "test3"
}
if crypto != "rsa" {
crypto = "gost"
}
dep := pickStr(s.NSD.DeponentCode, orgCode(s))
if dep == "" {
jsonOut(w, map[string]any{"ok": false, "error": "сначала укажите депозитарный код организации (шаг 4)"})
return
}
extra, _ := json.Marshal(map[string]string{"contour": contour, "crypto": crypto, "deponent": dep})
ctx, cancel := context.WithTimeout(r.Context(), 3*time.Minute)
defer cancel()
res, err := runKeymediaOp(ctx, "igate-contour", map[string]string{"extra": string(extra)})
if err == nil && res["ok"] == true && rc.Snapshot().NSD.Profile == "" {
// контур в bj ещё не выбран (новая установка) — первый настроенный канал и есть рабочий
n := rc.Snapshot().NSD
n.Profile, n.IGWBaseURL = contour+"-"+crypto, pickStr(n.IGWBaseURL, igateDefaultURL)
if rc.UpdateNSD(n) == nil {
res["output"] = asString(res["output"]) + ". bj работает с этим контуром и перезапускается — обновите страницу через 15 секунд"
go func() {
time.Sleep(2 * time.Second)
_, _ = restartUnit(context.Background(), "bj-server")
}()
}
}
refreshCertStore(rc)
jsonOut(w, opResult(res, err))
})
// переключить bj на контур: профиль отправки, база DEPO контура; bj перезапускается
mux.HandleFunc("/admin/setup/igate/switch", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method", http.StatusMethodNotAllowed)
return
}
contour, crypto := r.FormValue("contour"), r.FormValue("crypto")
if contour != "prod" {
contour = "test3"
}
if crypto != "rsa" {
crypto = "gost"
}
s := rc.Snapshot()
dep := pickStr(s.NSD.DeponentCode, orgCode(s))
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
defer cancel()
// канал контура должен быть в ИШ и включён (ключ контура установлен)
st, err := runKeymediaOp(ctx, "igate-status", nil)
if err != nil {
jsonOut(w, map[string]any{"ok": false, "error": "состояние ИШ: " + err.Error()})
return
}
want := contourChannel(contour) + dep
ready := false
if chans, ok := st["channels"].([]any); ok {
for _, c := range chans {
if m, ok := c.(map[string]any); ok && m["code"] == want && m["enabled"] == true {
ready = true
}
}
}
if !ready {
jsonOut(w, map[string]any{"ok": false, "error": "в ИШ нет включённого канала " + want + ": настройте канал контура и установите ключ этого контура"})
return
}
if contour == "prod" && r.FormValue("confirm") != "БОЕВОЙ" {
jsonOut(w, map[string]any{"ok": false, "error": "для перехода на боевой контур введите слово БОЕВОЙ"})
return
}
depoDB := strings.TrimSpace(r.FormValue("depo_db"))
if depoDB != "" && !validDBName(depoDB) {
jsonOut(w, map[string]any{"ok": false, "error": "имя базы DEPO — латинские буквы, цифры и _"})
return
}
n, f := s.NSD, s.Fancy
from := contourOf(n.Profile)
// запоминаем базу DEPO контура, с которого уходим, и берём базу контура, на который переходим
if from == "prod" {
n.DepoDBProd = f.DepoDB()
} else {
n.DepoDBTest = f.DepoDB()
}
target := depoDB
if target == "" {
if contour == "prod" {
target = n.DepoDBProd
} else {
target = n.DepoDBTest
}
}
n.Profile = contour + "-" + crypto
n.IGWBaseURL = pickStr(n.IGWBaseURL, igateDefaultURL)
n.SwitchedAt = time.Now()
nc := s.NSDCerts
nc.LastStore, nc.LastRows, nc.LastProfile = nil, nil, "" // справочник другого контура — перечитается после перезапуска
_ = rc.UpdateNSDCerts(nc)
if target != "" && target != f.DepoDB() {
f.DepoDatabase = target
f.DepoTablesCheck, f.DepoTablesCheckOK = "", false // таблицы и обработку в другой базе — проверить заново
f.DepoJobCheck, f.DepoJobCheckOK = "", false
f.DepoProcCheck, f.DepoProcCheckOK = "", false
if err := rc.UpdateFancy(f); err != nil {
jsonOut(w, map[string]any{"ok": false, "error": err.Error()})
return
}
}
if err := rc.UpdateNSD(n); err != nil {
jsonOut(w, map[string]any{"ok": false, "error": err.Error()})
return
}
log.Printf("nsd: контур переключён %s → %s (профиль %s, база DEPO %s)", from, contour, n.Profile, rc.Snapshot().Fancy.DepoDB())
go func() {
time.Sleep(2 * time.Second)
if _, err := restartUnit(context.Background(), "bj-server"); err != nil {
log.Printf("nsd: перезапуск bj-server после смены контура: %v", err)
}
}()
name := map[string]string{"prod": "боевой", "test3": "тестовый TEST3"}[contour]
msg := "bj переключён на " + name + " контур (база DEPO " + rc.Snapshot().Fancy.DepoDB() + ") и перезапускается — обновите страницу через 15 секунд"
if !rc.Snapshot().Fancy.DepoTablesCheckOK {
msg += ". База DEPO сменилась — пройдите шаг 3 (таблицы обмена и обработка в этой базе)"
}
jsonOut(w, map[string]any{"ok": true, "output": msg})
})
}
// refreshCertStore — перечитать справочник сертификатов ИШ сейчас (после установки ключа, настройки канала,
// смены контура), не дожидаясь плановой сверки раз в 6 часов: от него зависят шаг «Сертификаты» и сроки.
func refreshCertStore(rc *RuntimeConfig) {
go func() {
time.Sleep(20 * time.Second) // ИШ перезапускается после изменений
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
defer cancel()
if _, err := nsdCertsCheck(ctx, rc); err != nil {
log.Printf("nsdcerts: перечитать справочник: %v", err)
}
}()
}