Files
a301_server/internal/bossraid/handler.go
tolelom cc751653c4
All checks were successful
Server CI/CD / deploy (push) Successful in 1m26s
fix: 코드 리뷰 기반 보안·안정성 개선 2차
보안:
- RPC 응답 HTTP 상태코드 검증 (chain/client)
- SSAFY OAuth 에러 응답 내부 로깅으로 변경 (제3자 상세 노출 제거)
- resolveUsername에서 username 노출 제거
- LIKE 쿼리 특수문자 이스케이프 (bossraid/repository)
- 파일명 경로 순회 방지 + 길이 제한 (download/handler)
- ServerAuth 실패 로깅 추가

안정성:
- AutoMigrate 에러 시 서버 종료
- GetLatest() 에러 시 nil 반환 (초기화 안 된 포인터 방지)
- 멱등성 캐시 저장 시 새 context 사용
- SSAFY HTTP 클라이언트 타임아웃 10s
- io.ReadAll/rand.Read 에러 처리
- Login에서 DB 에러/Not Found 구분

검증 강화:
- 중복 플레이어 검증 (bossraid/service)
- username 길이 제한 50자 (auth/handler, bossraid/handler)
- 역할 변경 시 세션 무효화
- 지갑 복호화 실패 로깅

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 17:48:05 +09:00

147 lines
4.3 KiB
Go

package bossraid
import (
"log"
"github.com/gofiber/fiber/v2"
)
type Handler struct {
svc *Service
}
func NewHandler(svc *Service) *Handler {
return &Handler{svc: svc}
}
func bossError(c *fiber.Ctx, status int, userMsg string, err error) error {
log.Printf("bossraid error: %s: %v", userMsg, err)
return c.Status(status).JSON(fiber.Map{"error": userMsg})
}
// RequestEntry handles POST /api/internal/bossraid/entry
// Called by MMO server when a party requests boss raid entry.
func (h *Handler) RequestEntry(c *fiber.Ctx) error {
var req struct {
Usernames []string `json:"usernames"`
BossID int `json:"bossId"`
}
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "잘못된 요청입니다"})
}
if len(req.Usernames) == 0 || req.BossID <= 0 {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "usernames와 bossId는 필수입니다"})
}
for _, u := range req.Usernames {
if len(u) == 0 || len(u) > 50 {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "유효하지 않은 username입니다"})
}
}
room, err := h.svc.RequestEntry(req.Usernames, req.BossID)
if err != nil {
return bossError(c, fiber.StatusConflict, err.Error(), err)
}
return c.Status(fiber.StatusCreated).JSON(fiber.Map{
"roomId": room.ID,
"sessionName": room.SessionName,
"bossId": room.BossID,
"players": req.Usernames,
"status": room.Status,
})
}
// StartRaid handles POST /api/internal/bossraid/start
// Called by dedicated server when the Fusion session begins.
func (h *Handler) StartRaid(c *fiber.Ctx) error {
var req struct {
SessionName string `json:"sessionName"`
}
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "잘못된 요청입니다"})
}
if req.SessionName == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "sessionName은 필수입니다"})
}
room, err := h.svc.StartRaid(req.SessionName)
if err != nil {
return bossError(c, fiber.StatusBadRequest, err.Error(), err)
}
return c.JSON(fiber.Map{
"roomId": room.ID,
"sessionName": room.SessionName,
"status": room.Status,
})
}
// CompleteRaid handles POST /api/internal/bossraid/complete
// Called by dedicated server when the boss is killed. Distributes rewards.
func (h *Handler) CompleteRaid(c *fiber.Ctx) error {
var req struct {
SessionName string `json:"sessionName"`
Rewards []PlayerReward `json:"rewards"`
}
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "잘못된 요청입니다"})
}
if req.SessionName == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "sessionName은 필수입니다"})
}
room, results, err := h.svc.CompleteRaid(req.SessionName, req.Rewards)
if err != nil {
return bossError(c, fiber.StatusBadRequest, err.Error(), err)
}
return c.JSON(fiber.Map{
"roomId": room.ID,
"sessionName": room.SessionName,
"status": room.Status,
"rewardResults": results,
})
}
// FailRaid handles POST /api/internal/bossraid/fail
// Called by dedicated server on timeout or party wipe.
func (h *Handler) FailRaid(c *fiber.Ctx) error {
var req struct {
SessionName string `json:"sessionName"`
}
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "잘못된 요청입니다"})
}
if req.SessionName == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "sessionName은 필수입니다"})
}
room, err := h.svc.FailRaid(req.SessionName)
if err != nil {
return bossError(c, fiber.StatusBadRequest, err.Error(), err)
}
return c.JSON(fiber.Map{
"roomId": room.ID,
"sessionName": room.SessionName,
"status": room.Status,
})
}
// GetRoom handles GET /api/internal/bossraid/room
// Query param: sessionName
func (h *Handler) GetRoom(c *fiber.Ctx) error {
sessionName := c.Query("sessionName")
if sessionName == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "sessionName은 필수입니다"})
}
room, err := h.svc.GetRoom(sessionName)
if err != nil {
return bossError(c, fiber.StatusNotFound, "방을 찾을 수 없습니다", err)
}
return c.JSON(room)
}