Files
a301_server/main.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

206 lines
6.4 KiB
Go

package main
import (
"log"
"os"
"os/signal"
"syscall"
"time"
"a301_server/internal/announcement"
"a301_server/internal/auth"
"a301_server/internal/bossraid"
"a301_server/internal/chain"
"a301_server/internal/download"
"a301_server/internal/player"
"a301_server/internal/server"
_ "a301_server/docs" // swagger docs
"github.com/tolelom/tolchain/core"
"a301_server/pkg/config"
"a301_server/pkg/database"
"a301_server/pkg/middleware"
"a301_server/routes"
)
// @title One of the Plans API
// @version 1.0
// @description 멀티플레이어 보스 레이드 게임 플랫폼 백엔드 API
// @host a301.api.tolelom.xyz
// @BasePath /
// @securityDefinitions.apikey BearerAuth
// @in header
// @name Authorization
// @description JWT Bearer 토큰 (예: Bearer eyJhbGci...)
// @securityDefinitions.apikey ApiKeyAuth
// @in header
// @name X-API-Key
// @description 내부 API 키 (게임 서버 ↔ API 서버 통신용)
func main() {
config.Load()
config.WarnInsecureDefaults()
db, err := database.ConnectMySQL()
if err != nil {
log.Fatalf("MySQL 연결 실패: %v", err)
}
log.Println("MySQL 연결 성공")
// AutoMigrate
if err := db.AutoMigrate(&auth.User{}, &announcement.Announcement{}, &download.Info{}, &chain.UserWallet{}, &bossraid.BossRoom{}, &bossraid.DedicatedServer{}, &bossraid.RoomSlot{}, &bossraid.RewardFailure{}, &player.PlayerProfile{}); err != nil {
log.Fatalf("AutoMigrate 실패: %v", err)
}
rdb, err := database.ConnectRedis()
if err != nil {
log.Fatalf("Redis 연결 실패: %v", err)
}
log.Println("Redis 연결 성공")
// ── 의존성 주입 ──────────────────────────────────────────────────
authRepo := auth.NewRepository(db)
authSvc := auth.NewService(authRepo, rdb)
authHandler := auth.NewHandler(authSvc)
chainClient := chain.NewClient(config.C.ChainNodeURLs...)
chainRepo := chain.NewRepository(db)
chainSvc, err := chain.NewService(chainRepo, chainClient, config.C.ChainID, config.C.OperatorKeyHex, config.C.WalletEncryptionKey)
if err != nil {
log.Fatalf("chain service init failed: %v", err)
}
chainHandler := chain.NewHandler(chainSvc)
userResolver := func(username string) (uint, error) {
user, err := authRepo.FindByUsername(username)
if err != nil {
return 0, err
}
return user.ID, nil
}
chainSvc.SetUserResolver(userResolver)
authSvc.SetWalletCreator(func(userID uint) error {
_, err := chainSvc.CreateWallet(userID)
return err
})
playerRepo := player.NewRepository(db)
playerSvc := player.NewService(playerRepo)
playerSvc.SetUserResolver(userResolver)
playerHandler := player.NewHandler(playerSvc)
authSvc.SetProfileCreator(func(userID uint) error {
return playerSvc.CreateProfile(userID)
})
if err := authSvc.EnsureAdmin(config.C.AdminUsername, config.C.AdminPassword); err != nil {
log.Printf("admin 계정 생성 실패: %v", err)
} else {
log.Printf("admin 계정 확인 완료: %s", config.C.AdminUsername)
}
brRepo := bossraid.NewRepository(db)
brSvc := bossraid.NewService(brRepo, rdb)
brSvc.SetRewardGranter(func(username string, tokenAmount uint64, assets []core.MintAssetPayload) (string, error) {
result, err := chainSvc.GrantRewardByUsername(username, tokenAmount, assets)
if result != nil {
return result.TxID, err
}
return "", err
})
brSvc.SetExpGranter(func(username string, exp int) error {
return playerSvc.GrantExperienceByUsername(username, exp)
})
brHandler := bossraid.NewHandler(brSvc)
if config.C.InternalAPIKey == "" {
log.Println("WARN: INTERNAL_API_KEY not set — /api/internal/* endpoints are disabled")
}
annRepo := announcement.NewRepository(db)
annSvc := announcement.NewService(annRepo)
annHandler := announcement.NewHandler(annSvc)
dlRepo := download.NewRepository(db)
dlSvc := download.NewService(dlRepo, config.C.GameDir)
dlHandler := download.NewHandler(dlSvc, config.C.BaseURL)
// ── 서버 + 라우트 설정 ───────────────────────────────────────────
app := server.New()
authMw := middleware.Auth(rdb, config.C.JWTSecret)
serverAuthMw := middleware.ServerAuth(config.C.InternalAPIKey)
idempotencyReqMw := middleware.IdempotencyRequired(rdb)
routes.Register(app, authHandler, annHandler, dlHandler, chainHandler, brHandler, playerHandler,
server.AuthLimiter(), server.APILimiter(), server.HealthCheck(), server.ReadyCheck(db, rdb),
server.ChainUserLimiter(), authMw, serverAuthMw, idempotencyReqMw, server.RefreshLimiter())
// ── 백그라운드 워커 ──────────────────────────────────────────────
go func() {
ticker := time.NewTicker(15 * time.Second)
defer ticker.Stop()
for range ticker.C {
brSvc.CheckStaleSlots()
}
}()
rewardWorker := bossraid.NewRewardWorker(
brRepo,
func(username string, tokenAmount uint64, assets []core.MintAssetPayload) (string, error) {
result, err := chainSvc.GrantRewardByUsername(username, tokenAmount, assets)
if result != nil {
return result.TxID, err
}
return "", err
},
func(username string, exp int) error {
return playerSvc.GrantExperienceByUsername(username, exp)
},
func(txID string) (bool, error) {
result, err := chainClient.GetTxStatus(txID)
if err != nil {
return false, err
}
return result != nil && result.Success, nil
},
)
rewardWorker.Start()
// ── Graceful shutdown ────────────────────────────────────────────
go func() {
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
sig := <-sigCh
log.Printf("수신된 시그널: %v — 서버 종료 중...", sig)
rewardWorker.Stop()
if err := app.ShutdownWithTimeout(10 * time.Second); err != nil {
log.Printf("서버 종료 실패: %v", err)
}
if rdb != nil {
if err := rdb.Close(); err != nil {
log.Printf("Redis 종료 실패: %v", err)
} else {
log.Println("Redis 연결 종료 완료")
}
}
if sqlDB, err := db.DB(); err == nil {
if err := sqlDB.Close(); err != nil {
log.Printf("MySQL 종료 실패: %v", err)
} else {
log.Println("MySQL 연결 종료 완료")
}
}
}()
log.Fatal(app.Listen(":" + config.C.AppPort))
}