/
advanceddev
/
license-server
Обзор
Документация
Войти
/
advanceddev
/
license-server
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
2
CI/CD
Аналитика
Безопасность
master
internal/handler/license_handler.go
182 строки
5 KB
advanceddev
fix: security & reliability hardening for rate limiter, auth, and audit
10 авг 2026, 23:52
Не верифицирован
10 авг 2026, 23:52
aef6073
Код
Авторство
О чём код?
// Package handler -- package handler import ( "encoding/json" "errors" "fmt" "log/slog" "net/http" "time" "gitverse.ru/advanceddev/license-server/internal/domain" "gitverse.ru/advanceddev/license-server/internal/middleware" "gitverse.ru/advanceddev/license-server/internal/repository" "gitverse.ru/advanceddev/license-server/internal/validate" ) // LicenseHandler - обработчик запросов к лицензии. type LicenseHandler struct { repo domain.LicenseRepository cache *repository.LicenseCache audit *auditWriter limiter *middleware.RateLimiter keyLimiter *middleware.RateLimiter } // NewLicenseHandler - конструктор обработчика лицензий. func NewLicenseHandler( repo domain.LicenseRepository, cache *repository.LicenseCache, auditRepo domain.AuditRepository, limiter *middleware.RateLimiter, keyLimiter *middleware.RateLimiter, ) *LicenseHandler { return &LicenseHandler{repo: repo, cache: cache, limiter: limiter, audit: newAuditWriter(auditRepo), keyLimiter: keyLimiter} } // Validate - проверяет валидность лицензии по ключу. func (h *LicenseHandler) Validate(w http.ResponseWriter, r *http.Request) { start := time.Now() var req domain.ValidateRequest if err := validate.DecodeStrict(r.Body, &req); err != nil { if validate.IsDecodeError(err) { writeJSON(w, http.StatusBadRequest, domain.ValidateResponse{ Valid: false, Reason: err.Error(), }) return } if validate.IsValidationError(err) { writeJSON(w, http.StatusUnprocessableEntity, domain.ValidateResponse{ Valid: false, Reason: err.Error(), }) return } http.Error(w, "internal error", http.StatusInternalServerError) return } keyHash := domain.HashLicenseKey(req.LicenseKey) cacheHit := false if h.cache != nil { cached, err := h.cache.Get(r.Context(), keyHash) if err == nil && cached != nil { cacheHit = true slog.Info("license_validated", "status", "valid", "cache_hit", true, "duration_ms", time.Since(start).Milliseconds(), ) writeJSON(w, http.StatusOK, cached) return } } if h.limiter != nil { ip := r.Header.Get("X-Real-IP") if ip == "" { ip = r.RemoteAddr } if !h.limiter.Allow(r.Context(), ip) { slog.Warn("rate_limit_rejected", "type", "ip", "ip", ip, "duration_ms", time.Since(start).Milliseconds(), ) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusTooManyRequests) w.Write([]byte(`{"error":"rate limit exceeded"}`)) return } } if h.keyLimiter != nil { keyID := keyHash[:16] slog.Info("DEBUG_key_check", "key_id", keyID, "key_id_len", len(keyID), "full_hash", keyHash, "redis_key", fmt.Sprintf("rl:key:%s", keyID), ) if !h.keyLimiter.Allow(r.Context(), keyID) { slog.Warn("rate_limit_rejected", "type", "key", "key_hash_prefix", keyHash[:8], "duration_ms", time.Since(start).Milliseconds(), ) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusTooManyRequests) w.Write([]byte(`{"error":"rate limit exceeded for this key"}`)) return } } license, err := h.repo.GetByKeyHash(r.Context(), keyHash) if err != nil { if errors.Is(err, domain.ErrLicenseNotFound) { slog.Info("license_validated", "status", "not_found", "cache_hit", false, "duration_ms", time.Since(start).Milliseconds(), ) writeJSON(w, http.StatusOK, domain.ValidateResponse{ Valid: false, Reason: "license not found", }) return } slog.Error("license_validation_error", "error", err, "duration_ms", time.Since(start).Milliseconds(), ) http.Error(w, "internal error", http.StatusInternalServerError) return } now := time.Now() if now.After(license.ExpiresAt) { slog.Info("license_validated", "status", "expired", "partner_id", license.PartnerID, "cache_hit", false, "duration_ms", time.Since(start).Milliseconds(), ) writeJSON(w, http.StatusOK, domain.ValidateResponse{ Valid: false, Reason: "license expired", ExpiresAt: &license.ExpiresAt, }) return } resp := domain.ValidateResponse{ Valid: true, ExpiresAt: &license.ExpiresAt, Partner: &domain.PartnerConfig{ PartnerID: license.PartnerID, BrandName: license.BrandName, Params: license.Params, }, } if h.cache != nil { _ = h.cache.Set(r.Context(), keyHash, &resp) } slog.Info("license_validated", "status", "valid", "partner_id", license.PartnerID, "cache_hit", cacheHit, "duration_ms", time.Since(start).Milliseconds(), ) h.audit.Log(r, license.ID, domain.AuditActionValidated, nil) writeJSON(w, http.StatusOK, resp) } func writeJSON(w http.ResponseWriter, status int, data any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) json.NewEncoder(w).Encode(data) }