Files
a301_server/internal/server/server.go
tolelom cc9884bdfe
All checks were successful
Server CI/CD / lint-and-build (push) Successful in 34s
Server CI/CD / deploy (push) Successful in 50s
fix: 아키텍처 리뷰 HIGH/MEDIUM 이슈 10건 수정
HIGH (3건):
- 런처 파일 업로드 시 PE 헤더 검증 + 500MB 크기 제한 추가
- 체인 노드 URL 파싱 시 scheme/host 유효성 검증
- Dockerfile 비루트 사용자(app:1000) 실행

MEDIUM (7건):
- SSAFY username 충돌 시 랜덤 suffix로 최대 3회 재시도
- 내부 API username 검증 validID(256자) → validUsername(3~50자) 분리
- 동시 업로드 경합 방지 sync.Mutex 추가
- 프로덕션 환경변수 검증 강화 (DB_PASSWORD, OPERATOR_KEY_HEX, INTERNAL_API_KEY)
- Redis 에러 시 멱등성 요청 통과 → 503 거부로 변경
- CORS AllowOrigins 환경변수화 (CORS_ALLOW_ORIGINS)
- Refresh 엔드포인트 rate limiting 추가 (IP당 5 req/min)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 15:56:58 +09:00

131 lines
3.9 KiB
Go

package server
import (
"strconv"
"time"
"a301_server/pkg/apperror"
"a301_server/pkg/config"
"a301_server/pkg/metrics"
"a301_server/pkg/middleware"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/cors"
"github.com/gofiber/fiber/v2/middleware/limiter"
"github.com/gofiber/fiber/v2/middleware/logger"
"github.com/gofiber/fiber/v2/middleware/recover"
"github.com/redis/go-redis/v9"
"gorm.io/gorm"
)
// New creates a configured Fiber app with all global middleware applied.
func New() *fiber.App {
app := fiber.New(fiber.Config{
StreamRequestBody: true,
BodyLimit: 4 * 1024 * 1024 * 1024, // 4GB
ErrorHandler: middleware.ErrorHandler,
})
app.Use(recover.New(recover.Config{
EnableStackTrace: true,
}))
app.Use(middleware.RequestID)
app.Use(middleware.Metrics)
app.Get("/metrics", metrics.Handler)
app.Use(logger.New(logger.Config{
Format: `{"time":"${time}","status":${status},"latency":"${latency}","method":"${method}","path":"${path}","ip":"${ip}","reqId":"${locals:requestID}"}` + "\n",
TimeFormat: "2006-01-02T15:04:05Z07:00",
}))
app.Use(middleware.SecurityHeaders)
app.Use(cors.New(cors.Config{
AllowOrigins: config.C.CORSAllowOrigins,
AllowHeaders: "Origin, Content-Type, Authorization, Idempotency-Key, X-API-Key, X-Requested-With",
AllowMethods: "GET, POST, PUT, PATCH, DELETE",
AllowCredentials: true,
}))
return app
}
// AuthLimiter returns a rate limiter for auth endpoints (10 req/min per IP).
func AuthLimiter() fiber.Handler {
return limiter.New(limiter.Config{
Max: 10,
Expiration: 1 * time.Minute,
KeyGenerator: func(c *fiber.Ctx) string {
return c.IP()
},
LimitReached: func(c *fiber.Ctx) error {
return apperror.ErrRateLimited
},
})
}
// APILimiter returns a rate limiter for general API endpoints (60 req/min per IP).
func APILimiter() fiber.Handler {
return limiter.New(limiter.Config{
Max: 60,
Expiration: 1 * time.Minute,
KeyGenerator: func(c *fiber.Ctx) string {
return c.IP()
},
LimitReached: func(c *fiber.Ctx) error {
return apperror.ErrRateLimited
},
})
}
// RefreshLimiter returns a rate limiter for refresh token endpoint (5 req/min per IP).
// Separate from AuthLimiter to avoid NAT collisions while still preventing abuse.
func RefreshLimiter() fiber.Handler {
return limiter.New(limiter.Config{
Max: 5,
Expiration: 1 * time.Minute,
KeyGenerator: func(c *fiber.Ctx) string {
return "refresh:" + c.IP()
},
LimitReached: func(c *fiber.Ctx) error {
return apperror.ErrRateLimited
},
})
}
// ChainUserLimiter returns a rate limiter for chain transactions (20 req/min per user).
func ChainUserLimiter() fiber.Handler {
return limiter.New(limiter.Config{
Max: 20,
Expiration: 1 * time.Minute,
KeyGenerator: func(c *fiber.Ctx) string {
if uid, ok := c.Locals("userID").(uint); ok {
return "chain_user:" + strconv.FormatUint(uint64(uid), 10)
}
return "chain_ip:" + c.IP()
},
LimitReached: func(c *fiber.Ctx) error {
return apperror.ErrRateLimited
},
})
}
// HealthCheck returns a handler that reports server liveness.
func HealthCheck() fiber.Handler {
return func(c *fiber.Ctx) error {
return c.JSON(fiber.Map{"status": "ok"})
}
}
// ReadyCheck returns a handler that verifies DB and Redis connectivity.
func ReadyCheck(db *gorm.DB, rdb *redis.Client) fiber.Handler {
return func(c *fiber.Ctx) error {
sqlDB, err := db.DB()
if err != nil {
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"status": "error", "detail": "db pool"})
}
if err := sqlDB.Ping(); err != nil {
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"status": "error", "detail": "db"})
}
if err := rdb.Ping(c.Context()).Err(); err != nil {
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"status": "error", "detail": "redis"})
}
return c.JSON(fiber.Map{"status": "ok"})
}
}