feat: 보안 수정 + Prometheus 메트릭 + 단위 테스트 추가

보안:
- Zip Bomb 방어 (io.LimitReader 100MB)
- Redis Del 에러 로깅 (auth, idempotency)
- 로그인 실패 로그에서 username 제거
- os.Remove 에러 로깅

모니터링:
- Prometheus 메트릭 미들웨어 + /metrics 엔드포인트
- http_requests_total, http_request_duration_seconds 등 4개 메트릭

테스트:
- download (11), chain (10), bossraid (20) = 41개 단위 테스트

기타:
- DB 모델 GORM 인덱스 태그 추가
- launcherHash 필드 + hashFileToHex() 추가

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-18 10:37:42 +09:00
parent 82adb37ecb
commit 844a5b264b
14 changed files with 1016 additions and 495 deletions

54
pkg/metrics/metrics.go Normal file
View File

@@ -0,0 +1,54 @@
package metrics
import (
"io"
"net/http"
"net/http/httptest"
"github.com/gofiber/fiber/v2"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
var (
HTTPRequestsTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{Name: "http_requests_total", Help: "Total HTTP requests"},
[]string{"method", "path", "status"},
)
HTTPRequestDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{Name: "http_request_duration_seconds", Help: "HTTP request duration"},
[]string{"method", "path"},
)
DBConnectionsActive = prometheus.NewGauge(
prometheus.GaugeOpts{Name: "db_connections_active", Help: "Active DB connections"},
)
RedisConnectionsActive = prometheus.NewGauge(
prometheus.GaugeOpts{Name: "redis_connections_active", Help: "Active Redis connections"},
)
)
func init() {
prometheus.MustRegister(HTTPRequestsTotal, HTTPRequestDuration, DBConnectionsActive, RedisConnectionsActive)
}
// Handler returns a Fiber handler that serves the Prometheus metrics endpoint.
// It wraps promhttp.Handler() without requiring the gofiber/adaptor package.
func Handler(c *fiber.Ctx) error {
handler := promhttp.Handler()
req, err := http.NewRequest(http.MethodGet, "/metrics", nil)
if err != nil {
return c.SendStatus(fiber.StatusInternalServerError)
}
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
result := rec.Result()
defer result.Body.Close()
c.Set("Content-Type", result.Header.Get("Content-Type"))
c.Status(result.StatusCode)
body, err := io.ReadAll(result.Body)
if err != nil {
return c.SendStatus(fiber.StatusInternalServerError)
}
return c.Send(body)
}

View File

@@ -85,7 +85,9 @@ func Idempotency(c *fiber.Ctx) error {
// Processing failed — remove the key so it can be retried
delCtx, delCancel := context.WithTimeout(context.Background(), redisTimeout)
defer delCancel()
database.RDB.Del(delCtx, redisKey)
if delErr := database.RDB.Del(delCtx, redisKey).Err(); delErr != nil {
log.Printf("WARNING: idempotency cache delete failed (key=%s): %v", key, delErr)
}
return err
}
@@ -104,7 +106,9 @@ func Idempotency(c *fiber.Ctx) error {
// Non-success — allow retry by removing the key
delCtx, delCancel := context.WithTimeout(context.Background(), redisTimeout)
defer delCancel()
database.RDB.Del(delCtx, redisKey)
if delErr := database.RDB.Del(delCtx, redisKey).Err(); delErr != nil {
log.Printf("WARNING: idempotency cache delete failed (key=%s): %v", key, delErr)
}
}
return nil

25
pkg/middleware/metrics.go Normal file
View File

@@ -0,0 +1,25 @@
package middleware
import (
"strconv"
"time"
"a301_server/pkg/metrics"
"github.com/gofiber/fiber/v2"
)
// Metrics records HTTP request count and duration as Prometheus metrics.
func Metrics(c *fiber.Ctx) error {
start := time.Now()
err := c.Next()
duration := time.Since(start).Seconds()
status := strconv.Itoa(c.Response().StatusCode())
path := c.Route().Path // use route pattern to avoid cardinality explosion
method := c.Method()
metrics.HTTPRequestsTotal.WithLabelValues(method, path, status).Inc()
metrics.HTTPRequestDuration.WithLabelValues(method, path).Observe(duration)
return err
}