// Command bj-license-server — онлайн-сервис учёта и отзыва лицензий. // // Базовая модель лицензирования офлайновая: bj-server проверяет подпись и // срок сам. Этот сервер нужен для: // - реестра выданных лицензий (учёт); // - ОТЗЫВА (revocation) до окончания срока; // - проверки клиентом «не отозвана ли» (опциональный online-чек). // // Хранилище — JSON-файл со списком отозванных ID (для каркаса; в проде — // PostgreSQL). API: // // GET /v1/check?id= → {"revoked":bool} // GET /healthz // // Управление отзывом — правкой файла revoked.json (или будущим admin API). package main import ( "encoding/json" "flag" "log" "net/http" "os" "sync" "time" ) type store struct { mu sync.RWMutex path string revoked map[string]bool } func newStore(path string) *store { s := &store{path: path, revoked: map[string]bool{}} s.load() return s } func (s *store) load() { s.mu.Lock() defer s.mu.Unlock() b, err := os.ReadFile(s.path) if err != nil { return // файла нет — пустой список } var ids []string if err := json.Unmarshal(b, &ids); err != nil { log.Printf("license-server: разбор %s: %v", s.path, err) return } s.revoked = map[string]bool{} for _, id := range ids { s.revoked[id] = true } log.Printf("license-server: загружено отозванных лицензий: %d", len(s.revoked)) } func (s *store) isRevoked(id string) bool { s.mu.RLock() defer s.mu.RUnlock() return s.revoked[id] } func main() { addr := flag.String("addr", ":8091", "адрес прослушивания") file := flag.String("revoked", "./revoked.json", "JSON-файл со списком отозванных license ID") flag.Parse() st := newStore(*file) // Перечитываем файл отзывов раз в минуту (горячее применение). go func() { t := time.NewTicker(time.Minute) defer t.Stop() for range t.C { st.load() } }() mux := http.NewServeMux() mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte("ok")) }) mux.HandleFunc("/v1/check", func(w http.ResponseWriter, r *http.Request) { id := r.URL.Query().Get("id") if id == "" { http.Error(w, "id required", http.StatusBadRequest) return } w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(map[string]bool{"revoked": st.isRevoked(id)}) }) log.Printf("license-server: слушаю %s, отзывы из %s", *addr, *file) srv := &http.Server{Addr: *addr, Handler: mux, ReadHeaderTimeout: 10 * time.Second} log.Fatal(srv.ListenAndServe()) }