Files
Bridge-and-Join-s/internal/lkgateway/ish_version.go
T
fontvielle c9ab60130f ИШ: этапы обновления шлюза в окне обновления bj, проверка нового ИШ, свежая версия в строке НРД
- окно обновления bj после перезапуска ведёт обновление ИШ по этапам (загрузка, остановка,
  резервная копия, установка, запуск, проверка) и закрывается только после проверки;
  пока шлюз обновляется, то же окно на любой странице
- bj-igate-admin: этапы stop/backup/verify; после запуска сверяются версия, служба, движок,
  каналы и ключ с прежними (иначе автооткат), обмен с НРД
- версия ИШ в строке НРД читается всегда заново (кэш 6 ч — только сайт НРД);
  после установки версии пересобираются сразу

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q7eREaYwUt1i4fNLZcGnjn
2026-10-21 22:50:43 +03:00

219 lines
8.1 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
// ish_version.go — определение установленной версии Интеграционного шлюза НРД
// (ИШ) и сравнение с версией, доступной на сайте НРД.
//
// Установленная версия берётся из igate.deps.json (world-readable), доступная —
// со страницы дистрибутивов НРД. ВАЖНО: на странице соседствуют сборки под разные
// ОС с РАЗНЫМИ номерами (на момент внедрения Windows x86/x64 = 107.0.0.933, а
// Astra-Linux/amd64 = 107.0.0.930). Мы работаем на Linux (.deb, amd64), поэтому
// сравнивать нужно именно с Linux-сборкой, иначе система ложно покажет «доступно
// обновление» на версию, которой под нашу ОС ещё нет.
import (
"context"
"net/http"
"os"
"regexp"
"strings"
"sync"
"time"
)
// ishDepsPath — файл манифеста .NET-сборки ИШ; содержит "igate/<версия>".
const ishDepsPath = "/opt/nsd/igate/igate/igate.deps.json"
// ishSiteURL — страница дистрибутивов ПО НРД (там же ИШ).
const ishSiteURL = "https://www.nsd.ru/workflow/system/programs/web-service/"
var (
ishVerRe = regexp.MustCompile(`"igate/(\d+(?:\.\d+){2,3})"`)
ishAstraRe = regexp.MustCompile(`Astra-Linux[^"]*?(\d{2,3}(?:\.\d+){3})`)
ishAnyRe = regexp.MustCompile(`\d{2,3}(?:\.\d+){3}`)
)
// IshVersionInfo — сводка по версиям ИШ для отображения в интерфейсе.
type IshVersionInfo struct {
Installed string `json:"installed"` // напр. 107.0.930 (из deps.json)
SiteLinux string `json:"site_linux,omitempty"` // Astra-Linux сборка на сайте НРД
SiteLatest string `json:"site_latest,omitempty"` // самая новая любая ОС (инфо)
UpToDate bool `json:"up_to_date"` // installed соответствует Linux-сборке
Note string `json:"note,omitempty"` // человекочитаемый комментарий
CheckedAt time.Time `json:"checked_at"`
}
// ishInstalledVersion читает версию ИШ из deps.json. Пусто, если ИШ не найден.
func ishInstalledVersion() string {
b, err := os.ReadFile(ishDepsPath)
if err != nil {
return ""
}
m := ishVerRe.FindSubmatch(b)
if m == nil {
return ""
}
return string(m[1])
}
// canonVer приводит версию к форме без внутренних нулей: 107.0.0.930 и 107.0.930
// дают одинаковый результат (сборочная версия .deb опускает нулевой компонент).
func canonVer(v string) string {
p := strings.Split(strings.TrimSpace(v), ".")
if len(p) < 2 {
return v
}
core := []string{p[0]}
for _, x := range p[1 : len(p)-1] {
if x != "0" {
core = append(core, x)
}
}
core = append(core, p[len(p)-1])
return strings.Join(core, ".")
}
var (
ishVerMu sync.Mutex
ishVerCache *IshVersionInfo
ishVerTTL = 6 * time.Hour
)
// ishVersionInfo возвращает сводку по версиям ИШ (с кэшем на 6ч, чтобы не дёргать
// сайт НРД на каждый показ статуса). Сетевая часть best-effort: при недоступности
// сайта возвращается только установленная версия.
func ishVersionInfo(ctx context.Context) IshVersionInfo {
// установленную версию читаем всегда заново (ИШ мог обновиться минуту назад), сайт — из кэша
ishVerMu.Lock()
site := ishVerCache
ishVerMu.Unlock()
if site == nil || time.Since(site.CheckedAt) >= ishVerTTL {
linux, latest := ishSiteVersions(ctx)
site = &IshVersionInfo{SiteLinux: linux, SiteLatest: latest, CheckedAt: time.Now().UTC()}
ishVerMu.Lock()
ishVerCache = site
ishVerMu.Unlock()
}
info := IshVersionInfo{
Installed: ishInstalledVersion(),
SiteLinux: site.SiteLinux,
SiteLatest: site.SiteLatest,
CheckedAt: site.CheckedAt,
}
linux, latest := info.SiteLinux, info.SiteLatest
switch {
case info.Installed == "":
info.Note = "ИШ не обнаружен (deps.json недоступен)"
case linux == "":
info.UpToDate = true // не с чем сравнивать — не тревожим
info.Note = "версия на сайте НРД недоступна (проверьте вручную)"
case canonVer(info.Installed) == canonVer(linux):
info.UpToDate = true
info.Note = "актуальная версия для Linux (Astra/amd64)"
if latest != "" && canonVer(latest) != canonVer(linux) {
info.Note += "; для Windows доступна " + latest
}
default:
info.UpToDate = false
info.Note = "доступно обновление до " + linux + " (Astra-Linux)"
}
return info
}
// ishSiteVersions скачивает страницу НРД и возвращает (Linux-сборка, самая новая
// по всем ОС). Обе строки пусты при ошибке сети/парсинга.
func ishSiteVersions(ctx context.Context) (linux, latest string) {
c := &http.Client{Timeout: 15 * time.Second}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, ishSiteURL, nil)
if err != nil {
return "", ""
}
// WAF сайта НРД отвечает 403 на «Mozilla»-UA без заголовка Accept (эвристика
// анти-бот). Отправляем полноценный браузерный набор заголовков.
req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0")
req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
req.Header.Set("Accept-Language", "ru-RU,ru;q=0.9,en;q=0.8")
resp, err := c.Do(req)
if err != nil {
return "", ""
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", ""
}
body := make([]byte, 0, 256*1024)
buf := make([]byte, 32*1024)
for len(body) < 4*1024*1024 { // не читаем больше 4 МБ
n, rerr := resp.Body.Read(buf)
body = append(body, buf[:n]...)
if rerr != nil {
break
}
}
if m := ishAstraRe.FindSubmatch(body); m != nil {
linux = string(m[1])
}
// Самая новая версия по всем сборкам (для информационной строки).
var best string
for _, v := range ishAnyRe.FindAllString(string(body), -1) {
if best == "" || verLess(best, v) {
best = v
}
}
latest = best
return linux, latest
}
// verLess сравнивает версии покомпонентно как числа: verLess(a,b)=true если a<b.
func verLess(a, b string) bool {
pa, pb := strings.Split(a, "."), strings.Split(b, ".")
for i := 0; i < len(pa) || i < len(pb); i++ {
var x, y int
if i < len(pa) {
x = atoiSafe(pa[i])
}
if i < len(pb) {
y = atoiSafe(pb[i])
}
if x != y {
return x < y
}
}
return false
}
func atoiSafe(s string) int {
n := 0
for _, r := range s {
if r < '0' || r > '9' {
return n
}
n = n*10 + int(r-'0')
}
return n
}
// ishVersionStatus — версия ИШ как отдельная проверка для панели статуса.
func ishVersionStatus(ctx context.Context) Status {
info := ishVersionInfo(ctx)
s := Status{Name: "Версия Интеграционного шлюза", Optional: true}
if info.Installed == "" {
s.OK = true
s.Message = "ИШ не обнаружен на этом сервере"
return s
}
s.OK = info.UpToDate
if info.UpToDate {
s.Message = "установлена " + info.Installed + " — " + info.Note
} else {
s.Message = "установлена " + info.Installed + "; " + info.Note
}
if info.SiteLinux != "" {
s.Detail = "на сайте НРД (Linux): " + info.SiteLinux
if info.SiteLatest != "" && info.SiteLatest != info.SiteLinux {
s.Detail += ", последняя (Windows): " + info.SiteLatest
}
}
return s
}