package lkgateway // Интеграционный шлюз НРД (ИШ, служба igate) на этом сервере: состояние для мастера (шаг 8). 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"] = 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 } 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() jsonOut(w, opResult(runKeymediaOp(ctx, "igate-contour", map[string]string{"extra": string(extra)}))) }) // переключить 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() 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] jsonOut(w, map[string]any{"ok": true, "output": "bj переключён на " + name + " контур (база DEPO " + rc.Snapshot().Fancy.DepoDB() + ") и перезапускается — обновите страницу через 15 секунд"}) }) }