f296ac5ad8
readHandbook брал самый свежий из своего, общего (с сервера лицензий) и встроенного — общий был тестовым (TEST3, 166 участников, прислан с 28), и на боевом 9 bj считал «регистрация подтверждена», хотя в боевом справочнике НРД (89) нас нет. Теперь при своём справочнике от НРД — только он; на сервер лицензий справочник уходит только с боевого контура. Регистрация участника проверяется по справочнику, полученному от НРД; появились в нём — анкета отмечается принятой автоматически, плашка уходит. Портфель ЛК: клиенты DEPO списком по клику (первые 50 по алфавиту), «не найдено» вместо «поиск недоступен» (пустой ответ был null). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q7eREaYwUt1i4fNLZcGnjn
155 lines
5.7 KiB
Go
155 lines
5.7 KiB
Go
package lkgateway
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
_ "embed"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Справочник участников M2M (открытые данные НРД). Источники, из которых берётся самый свежий:
|
|
// - полученный от НРД этим bj (M2MTH через шлюз НРД) — handbookFile;
|
|
// - копия с сервера лицензий (её присылает установка со шлюзом НРД, помеченная источником) —
|
|
// handbookCloudFile, забирается раз в сутки;
|
|
// - снимок, встроенный в bj (чистая установка без шлюза и без связи).
|
|
//
|
|
//go:embed handbook/m2m-handbook.json
|
|
var handbookSnapshot []byte
|
|
|
|
const handbookCloudFile = "/var/lib/bj/.bj/m2m-handbook-cloud.json"
|
|
|
|
func handbookAt(b []byte) time.Time {
|
|
var h struct {
|
|
At time.Time `json:"at"`
|
|
}
|
|
_ = json.Unmarshal(b, &h)
|
|
return h.At
|
|
}
|
|
|
|
// readHandbook — справочник своего контура: полученный этим bj от НРД, если он есть (боевой и
|
|
// тестовый справочники разные — в боевом меньше участников, не все подключились; копия с сервера
|
|
// лицензий может быть с другого контура). Нет своего — самый свежий из копии и встроенного снимка.
|
|
func readHandbook() ([]byte, error) {
|
|
if b, err := os.ReadFile(handbookFile); err == nil && len(b) > 0 && !handbookAt(b).IsZero() {
|
|
return b, nil
|
|
}
|
|
best := handbookSnapshot
|
|
for _, p := range []string{handbookCloudFile} {
|
|
if b, err := os.ReadFile(p); err == nil && len(b) > 0 && handbookAt(b).After(handbookAt(best)) {
|
|
best = b
|
|
}
|
|
}
|
|
return best, nil
|
|
}
|
|
|
|
func handbookURL(rc *RuntimeConfig) string {
|
|
base := strings.TrimSpace(rc.Snapshot().Update.BaseURL)
|
|
if base == "" {
|
|
base = DefaultUpdateURL
|
|
}
|
|
return strings.TrimRight(base, "/") + "/v1/handbook"
|
|
}
|
|
|
|
// pushHandbookAsync — отправить полученный от НРД справочник на сервер лицензий. Сервер примет его,
|
|
// только если лицензия отмечена как источник справочника; у остальных — тихий отказ.
|
|
func pushHandbookAsync(rc *RuntimeConfig, raw []byte) {
|
|
if rc == nil || strings.TrimSpace(rc.Snapshot().License.Key) == "" {
|
|
return
|
|
}
|
|
if contourOf(currentNSDProfile()) != "prod" {
|
|
return // общий справочник — только боевой: тестовый (TEST3) шире и для клиентов неверен
|
|
}
|
|
go func() {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPut, handbookURL(rc), bytes.NewReader(raw))
|
|
if err != nil {
|
|
return
|
|
}
|
|
req.Header.Set("Authorization", "Bearer "+strings.TrimSpace(rc.Snapshot().License.Key))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp, err := licenseHTTP.Do(req)
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode == http.StatusNoContent {
|
|
log.Printf("lk-gateway: справочник участников отправлен на сервер лицензий")
|
|
}
|
|
}()
|
|
}
|
|
|
|
// fetchHandbook — забрать справочник с сервера лицензий, если он свежее уже имеющихся.
|
|
func fetchHandbook(ctx context.Context, rc *RuntimeConfig) error {
|
|
key := strings.TrimSpace(rc.Snapshot().License.Key)
|
|
if key == "" {
|
|
return nil
|
|
}
|
|
if fi, err := os.Stat(handbookFile); err == nil && fi.Size() > 0 {
|
|
return nil // справочник уже приходит от НРД через шлюз — к серверу лицензий не обращаемся
|
|
}
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, handbookURL(rc), nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req.Header.Set("Authorization", "Bearer "+key)
|
|
resp, err := licenseHTTP.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode == http.StatusNotFound {
|
|
return nil
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
return fmt.Errorf("HTTP %d", resp.StatusCode)
|
|
}
|
|
b, err := io.ReadAll(io.LimitReader(resp.Body, 4<<20))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var doc struct {
|
|
At time.Time `json:"at"`
|
|
Participants []json.RawMessage `json:"participants"`
|
|
}
|
|
if json.Unmarshal(b, &doc) != nil || len(doc.Participants) == 0 || doc.At.IsZero() {
|
|
return fmt.Errorf("не справочник")
|
|
}
|
|
cur, _ := readHandbook()
|
|
if !doc.At.After(handbookAt(cur)) {
|
|
return nil
|
|
}
|
|
if err := os.WriteFile(handbookCloudFile, b, 0o600); err != nil {
|
|
return err
|
|
}
|
|
log.Printf("lk-gateway: справочник участников обновлён с сервера лицензий (%d участников, от %s)", len(doc.Participants), doc.At.Format("02.01.2006 15:04"))
|
|
return nil
|
|
}
|
|
|
|
// handbookLoop — пока своего справочника от НРД нет (новая установка без шлюза), раз в сутки
|
|
// забирает копию с сервера лицензий; как только справочник пришёл от НРД — больше не обращается.
|
|
func handbookLoop(ctx context.Context, rc *RuntimeConfig) {
|
|
t := time.NewTimer(2 * time.Minute)
|
|
defer t.Stop()
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-t.C:
|
|
}
|
|
cctx, cancel := context.WithTimeout(ctx, time.Minute)
|
|
if err := fetchHandbook(cctx, rc); err != nil {
|
|
log.Printf("lk-gateway: справочник с сервера лицензий: %v", err)
|
|
}
|
|
cancel()
|
|
t.Reset(24 * time.Hour)
|
|
}
|
|
}
|