feat: 일회용 ticket을 서버에서 JWT로 교환하는 로직 추가

브라우저에서 a301://launch?ticket=<hex> 형태로 호출 시
POST /api/auth/redeem-ticket으로 JWT를 받아 게임에 전달.
기존 token 파라미터 하위 호환 유지.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-17 21:28:36 +09:00
parent 574a6ee277
commit a28510df57

60
main.go
View File

@@ -2,6 +2,7 @@ package main
import ( import (
"archive/zip" "archive/zip"
"bytes"
"crypto/sha256" "crypto/sha256"
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
@@ -28,6 +29,7 @@ const (
protocolName = "a301" protocolName = "a301"
gameExeName = "A301.exe" gameExeName = "A301.exe"
serverInfoURL = "https://a301.api.tolelom.xyz/api/download/info" serverInfoURL = "https://a301.api.tolelom.xyz/api/download/info"
redeemTicketURL = "https://a301.api.tolelom.xyz/api/auth/redeem-ticket"
) )
const maxDownloadSize = 2 << 30 // 2GB const maxDownloadSize = 2 << 30 // 2GB
@@ -883,6 +885,38 @@ func ensureGame(gameDir, gamePath string, serverInfo *downloadInfo) error {
return nil return nil
} }
// ── Ticket redemption ────────────────────────────────────────────────────────
func redeemTicket(ticket string) (string, error) {
body, err := json.Marshal(map[string]string{"ticket": ticket})
if err != nil {
return "", fmt.Errorf("요청 생성 실패: %w", err)
}
resp, err := apiClient.Post(redeemTicketURL, "application/json", bytes.NewReader(body))
if err != nil {
return "", fmt.Errorf("서버 연결 실패: %w", err)
}
defer resp.Body.Close()
var result struct {
Token string `json:"token"`
Error string `json:"error"`
}
if err := json.NewDecoder(io.LimitReader(resp.Body, 1<<20)).Decode(&result); err != nil {
return "", fmt.Errorf("서버 응답 파싱 실패: %w", err)
}
if resp.StatusCode != 200 {
if result.Error != "" {
return "", fmt.Errorf("티켓 교환 실패: %s", result.Error)
}
return "", fmt.Errorf("티켓 교환 실패 (HTTP %d)", resp.StatusCode)
}
if result.Token == "" {
return "", fmt.Errorf("서버에서 토큰을 받지 못했습니다")
}
return result.Token, nil
}
// ── URI handler ────────────────────────────────────────────────────────────── // ── URI handler ──────────────────────────────────────────────────────────────
func handleURI(rawURI string) error { func handleURI(rawURI string) error {
@@ -895,13 +929,27 @@ func handleURI(rawURI string) error {
return fmt.Errorf("지원하지 않는 URI 스킴: %s", parsed.Scheme) return fmt.Errorf("지원하지 않는 URI 스킴: %s", parsed.Scheme)
} }
token := parsed.Query().Get("token") // ticket 파라미터로 일회용 티켓을 받아 서버에서 JWT로 교환
if token == "" { ticket := parsed.Query().Get("ticket")
if ticket == "" {
// 하위 호환: token 파라미터도 지원
ticket = parsed.Query().Get("token")
}
if ticket == "" {
return fmt.Errorf("토큰이 없습니다") return fmt.Errorf("토큰이 없습니다")
} }
// JWT는 점(.)으로 구분된 3파트 형식이어야 함
if parts := strings.Split(token, "."); len(parts) != 3 { var token string
return fmt.Errorf("유효하지 않은 토큰 형식입니다") if strings.Count(ticket, ".") == 2 {
// 이미 JWT 형식이면 그대로 사용
token = ticket
} else {
// 일회용 티켓 → 서버에서 JWT로 교환
t, err := redeemTicket(ticket)
if err != nil {
return fmt.Errorf("인증 실패: %w", err)
}
token = t
} }
gameDir, err := installDir() gameDir, err := installDir()
@@ -990,7 +1038,7 @@ func main() {
case strings.HasPrefix(arg, protocolName+"://"): case strings.HasPrefix(arg, protocolName+"://"):
if err := handleURI(arg); err != nil { if err := handleURI(arg); err != nil {
msgBox("One of the plans 런처 - 오류", fmt.Sprintf("실행 실패:\n%v", err), mbOK|mbError) msgBox("One of the plans 런처 - 오류", fmt.Sprintf("실행 실패:\n%v\n\n수신된 URI:\n%s", err, arg), mbOK|mbError)
os.Exit(1) os.Exit(1)
} }