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) } }