package lkgateway // Онлайн-лицензирование: активация установки кодом и ежесуточное продление ключа на // сервере лицензий (docs/license-server-api.md). Адрес сервера и открытый ключ зашиты в bj // и в настройках не меняются. Офлайн-ключ для закрытого контура вставляется вручную. import ( "bytes" "context" "crypto/rand" "crypto/sha256" "encoding/hex" "encoding/json" "errors" "fmt" "git.zetit.ru/zuevav/Bridge-and-Join-s/internal/hwid" "git.zetit.ru/zuevav/Bridge-and-Join-s/internal/license" "io" "log" "net/http" "os" "strings" "time" ) // licenseServer — сервер лицензий. var — только для подмены в тестах. var licenseServer = "https://lic.zetit.ru" var licenseHTTP = &http.Client{Timeout: 25 * time.Second} // errLicenseOffline — у установки офлайн-ключ, продлевать на сервере нечего. var errLicenseOffline = errors.New("ключ офлайн (закрытый контур): продление на сервере лицензий не выполняется") var licenseAlertAt time.Time // ensureInstallID — идентификатор установки: создаётся один раз и хранится в настройках. func ensureInstallID(rc *RuntimeConfig) string { s := rc.Snapshot().License if uuidRe.MatchString(s.InstallID) { return strings.ToLower(s.InstallID) } b := make([]byte, 16) _, _ = rand.Read(b) b[6] = b[6]&0x0f | 0x40 b[8] = b[8]&0x3f | 0x80 h := hex.EncodeToString(b) s.InstallID = h[0:8] + "-" + h[8:12] + "-" + h[12:16] + "-" + h[16:20] + "-" + h[20:] if err := rc.SaveLicense(s); err != nil { log.Printf("lk-gateway: лицензия: сохранение идентификатора установки: %v", err) } return s.InstallID } // machineHash — sha256 от /etc/machine-id: серверу лицензий, чтобы заметить копию сервера. func machineHash() string { b, err := os.ReadFile("/etc/machine-id") if err != nil || strings.TrimSpace(string(b)) == "" { return "" } sum := sha256.Sum256([]byte(strings.TrimSpace(string(b)))) return hex.EncodeToString(sum[:]) } type licenseAPIError struct { Status int `json:"-"` Code string `json:"error"` Message string `json:"message"` } func (e *licenseAPIError) Error() string { return e.Message } type licenseReply struct { Token string `json:"token"` LicenseID string `json:"license_id"` Tenant string `json:"tenant"` ExpiresAt time.Time `json:"expires_at"` ContractUntil time.Time `json:"contract_until"` } func licenseCall(ctx context.Context, path string, body map[string]string) (*licenseReply, error) { host, _ := os.Hostname() body["hostname"], body["version"], body["machine"] = host, BuildVersion, machineHash() body["board"], body["mac"] = hwid.Board(), hwid.MAC() // отпечаток сервера: хэши, без самих значений b, _ := json.Marshal(body) req, err := http.NewRequestWithContext(ctx, http.MethodPost, licenseServer+path, bytes.NewReader(b)) if err != nil { return nil, err } req.Header.Set("Content-Type", "application/json") req.Header.Set("User-Agent", "bj-server/"+BuildVersion) resp, err := licenseHTTP.Do(req) if err != nil { return nil, fmt.Errorf("сервер лицензий недоступен: %w", err) } defer resp.Body.Close() raw, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) if resp.StatusCode != http.StatusOK { var e licenseAPIError if json.Unmarshal(raw, &e) == nil && e.Code != "" { e.Status = resp.StatusCode return nil, &e } return nil, fmt.Errorf("сервер лицензий: HTTP %d", resp.StatusCode) } var r licenseReply if err := json.Unmarshal(raw, &r); err != nil || r.Token == "" { return nil, fmt.Errorf("сервер лицензий вернул непонятный ответ") } return &r, nil } // licenseDeactivate — сервер отключён от лицензии в портале клиента (или лицензия отозвана): ключ // удаляется сразу, не дожидаясь его срока, — модули и обновления отключаются. Вернуть: ввести ключ // продукта заново (если в лицензии есть свободное место). func licenseDeactivate(rc *RuntimeConfig, reason string) { s := rc.Snapshot().License if s.Key == "" { return } s.Key = "" s.LastAttempt, s.LastOK, s.LastMsg = time.Now(), false, "лицензия отключена: "+reason if err := rc.SaveLicense(s); err != nil { log.Printf("lk-gateway: отключение лицензии: %v", err) return } log.Printf("lk-gateway: лицензия на этом сервере отключена: %s", reason) raiseSystemAlert(rc, "Лицензия на этом сервере отключена", "Причина: "+reason+". Модули по лицензии и обновления отключены. Если это ошибка — введите ключ продукта заново в «Настройка → Лицензия» (нужно свободное место в лицензии) или обратитесь в ZETIT.") } // licenseRecord — итог обращения к серверу лицензий (для карточки); token != "" — новый ключ. func licenseRecord(rc *RuntimeConfig, token, msg string, ok bool) error { s := rc.Snapshot().License now := time.Now() s.LastAttempt, s.LastOK, s.LastMsg = now, ok, msg if ok { s.LastSync = now } if token != "" { s.Key = token } return rc.SaveLicense(s) } // licenseAccept — проверить ключ от сервера (подпись зашитым ключом, эта установка) и сохранить. func licenseAccept(rc *RuntimeConfig, token, how string) error { installID := ensureInstallID(rc) lic, err := verifyLicenseToken(token) if err != nil { return fmt.Errorf("сервер вернул недействительный ключ: %w", err) } if !strings.EqualFold(lic.InstallID, installID) { return fmt.Errorf("сервер вернул ключ для другой установки") } return licenseRecord(rc, token, fmt.Sprintf("%s: ключ до %s", how, lic.ExpiresAt.Local().Format("02.01.2006")), true) } // LicenseActivate — активация этой установки кодом на сервере лицензий. func LicenseActivate(ctx context.Context, rc *RuntimeConfig, code string) error { installID := ensureInstallID(rc) r, err := licenseCall(ctx, "/v1/activate", map[string]string{"code": strings.TrimSpace(code), "install_id": installID}) if err != nil { _ = licenseRecord(rc, "", "активация не удалась: "+err.Error(), false) return err } if err := licenseAccept(rc, r.Token, "активирована"); err != nil { _ = licenseRecord(rc, "", err.Error(), false) return err } log.Printf("lk-gateway: лицензия активирована на сервере лицензий: %s, лицензия %s", r.Tenant, r.LicenseID) afterLicenseChange(rc) return nil } // LicenseRenew — продление ключа этой установки. func LicenseRenew(ctx context.Context, rc *RuntimeConfig) error { installID := ensureInstallID(rc) cur := rc.Snapshot().License.Key lic, err := verifyLicenseToken(cur) if err != nil || lic.InstallID == "" { return errLicenseOffline } r, err := licenseCall(ctx, "/v1/renew", map[string]string{"token": cur, "install_id": installID}) if err != nil { _ = licenseRecord(rc, "", "продление не удалось: "+err.Error(), false) var ae *licenseAPIError if errors.As(err, &ae) && (ae.Code == "install_released" || ae.Code == "install_blocked" || ae.Code == "revoked") { licenseDeactivate(rc, ae.Message) return err } if errors.As(err, &ae) && time.Since(licenseAlertAt) > 12*time.Hour { licenseAlertAt = time.Now() raiseSystemAlert(rc, "Лицензия: сервер лицензий отказал в продлении", fmt.Sprintf("%s (%s). Текущий ключ действует до %s; если его не продлить, модули по лицензии отключатся — обратитесь в ZETIT.", ae.Message, ae.Code, lic.ExpiresAt.Local().Format("02.01.2006"))) } return err } if err := licenseAccept(rc, r.Token, "продлена"); err != nil { return err } if nl, err := verifyLicenseToken(r.Token); err == nil && licenseScopeChanged(lic, nl) { afterLicenseChange(rc) // в лицензию добавили модуль или план — поставить, не дожидаясь перезапуска bj } else { enforceModuleLicenseAsync(rc) // состав лицензии мог измениться — службы модулей в соответствие } return nil } // licenseScopeChanged — изменился ли состав лицензии: план или признаки модулей. func licenseScopeChanged(a, b *license.License) bool { if a == nil || b == nil { return a != b } if a.Plan != b.Plan || len(a.Features) != len(b.Features) { return true } for _, f := range b.Features { if !a.HasFeature(f) { return true } } return false } // afterLicenseChange — лицензию активировали или изменили её состав: службы модулей — в соответствие // лицензии, а модули, которые в неё входят, но ещё не стоят, ставятся сразу. На сервере с образа ключ // вводят уже после установки, и без этого модули (служба подписи и др.) встали бы только после // перезапуска bj. func afterLicenseChange(rc *RuntimeConfig) { go func() { ctx, cancel := context.WithTimeout(context.Background(), 12*time.Minute) defer cancel() enforceModuleLicense(ctx, rc) if _, err := os.Stat(keymediaReqDir); err != nil { return // нет root-службы (разработка, тесты) } _, _ = NewUpdater(rc).ensureSplitModules(ctx) }() } // licenseNeedsRenew — онлайн-ключ и 15 минут без продления: так режим разработки, включённый или // выключенный на сервере лицензий, доходит до установки быстро. После неудачи — не чаще раза в час. func licenseNeedsRenew(rc *RuntimeConfig) bool { s := rc.Snapshot().License lic, err := verifyLicenseToken(s.Key) if err != nil || lic.InstallID == "" { return false } if !s.LastOK && !s.LastAttempt.IsZero() && time.Since(s.LastAttempt) < time.Hour && time.Since(s.LastSync) < 23*time.Hour { return false } return time.Since(s.LastSync) >= 15*time.Minute } // licenseLoop — раз в 5 минут предупреждает о скором окончании лицензии и проверяет, пора ли продлить // ключ (продление — раз в сутки, при сбое — повтор через час). func (s *Server) licenseLoop(ctx context.Context) { ensureInstallID(s.rc) wait := 2 * time.Minute for { select { case <-ctx.Done(): return case <-time.After(wait): } wait = 5 * time.Minute licenseExpiryWarn(s.rc, time.Now()) disableEmulatorsIfDevOff(s.rc) // dev-режим может истечь и просто по времени — эмуляторы гасим if !licenseNeedsRenew(s.rc) { continue } cctx, cancel := context.WithTimeout(ctx, time.Minute) if err := LicenseRenew(cctx, s.rc); err != nil { log.Printf("lk-gateway: лицензия: %v", err) } else { log.Printf("lk-gateway: лицензия продлена: %s", s.rc.Snapshot().License.LastMsg) } cancel() } }