From a28510df571e965d8c7cced5e5f9ad9fd69991a0 Mon Sep 17 00:00:00 2001 From: tolelom <98kimsungmin@naver.com> Date: Tue, 17 Mar 2026 21:28:36 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=9D=BC=ED=9A=8C=EC=9A=A9=20ticket?= =?UTF-8?q?=EC=9D=84=20=EC=84=9C=EB=B2=84=EC=97=90=EC=84=9C=20JWT=EB=A1=9C?= =?UTF-8?q?=20=EA=B5=90=ED=99=98=ED=95=98=EB=8A=94=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 브라우저에서 a301://launch?ticket= 형태로 호출 시 POST /api/auth/redeem-ticket으로 JWT를 받아 게임에 전달. 기존 token 파라미터 하위 호환 유지. Co-Authored-By: Claude Opus 4.6 (1M context) --- main.go | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 55 insertions(+), 7 deletions(-) diff --git a/main.go b/main.go index 0fccfef..6f4617e 100644 --- a/main.go +++ b/main.go @@ -2,6 +2,7 @@ package main import ( "archive/zip" + "bytes" "crypto/sha256" "encoding/hex" "encoding/json" @@ -27,7 +28,8 @@ import ( const ( protocolName = "a301" 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 @@ -883,6 +885,38 @@ func ensureGame(gameDir, gamePath string, serverInfo *downloadInfo) error { 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 ────────────────────────────────────────────────────────────── func handleURI(rawURI string) error { @@ -895,13 +929,27 @@ func handleURI(rawURI string) error { return fmt.Errorf("지원하지 않는 URI 스킴: %s", parsed.Scheme) } - token := parsed.Query().Get("token") - if token == "" { + // ticket 파라미터로 일회용 티켓을 받아 서버에서 JWT로 교환 + ticket := parsed.Query().Get("ticket") + if ticket == "" { + // 하위 호환: token 파라미터도 지원 + ticket = parsed.Query().Get("token") + } + if ticket == "" { return fmt.Errorf("토큰이 없습니다") } - // JWT는 점(.)으로 구분된 3파트 형식이어야 함 - if parts := strings.Split(token, "."); len(parts) != 3 { - return fmt.Errorf("유효하지 않은 토큰 형식입니다") + + var token string + 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() @@ -990,7 +1038,7 @@ func main() { case strings.HasPrefix(arg, protocolName+"://"): 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) }