refactor: 단일 파일을 main/ui/download/protocol 4개 파일로 분리
- main.go: 진입점(main), handleURI, version - ui.go: Win32 UI (progress window, DPI, 폰트, 메시지박스) - download.go: 다운로드/추출 로직 (HTTP client, extractZip, doDownload) - protocol.go: 레지스트리 등록/해제, ensureGame, ensureLauncher, 서버 API Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
181
protocol.go
Normal file
181
protocol.go
Normal file
@@ -0,0 +1,181 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/windows/registry"
|
||||
)
|
||||
|
||||
const (
|
||||
protocolName = "a301"
|
||||
gameExeName = "A301.exe"
|
||||
serverInfoURL = "https://a301.api.tolelom.xyz/api/download/info"
|
||||
redeemTicketURL = "https://a301.api.tolelom.xyz/api/auth/redeem-ticket"
|
||||
)
|
||||
|
||||
// errNoRetry wraps errors that should not be retried (e.g. 4xx responses).
|
||||
type errNoRetry struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (e *errNoRetry) Error() string { return e.err.Error() }
|
||||
func (e *errNoRetry) Unwrap() error { return e.err }
|
||||
|
||||
type downloadInfo struct {
|
||||
FileHash string `json:"fileHash"`
|
||||
URL string `json:"url"`
|
||||
LauncherURL string `json:"launcherUrl"`
|
||||
LauncherHash string `json:"launcherHash"`
|
||||
}
|
||||
|
||||
// installDir returns the fixed install directory: %LOCALAPPDATA%\A301
|
||||
func installDir() (string, error) {
|
||||
localAppData := os.Getenv("LOCALAPPDATA")
|
||||
if localAppData == "" {
|
||||
return "", fmt.Errorf("LOCALAPPDATA 환경변수를 찾을 수 없습니다")
|
||||
}
|
||||
return filepath.Join(localAppData, "A301"), nil
|
||||
}
|
||||
|
||||
// launcherPath returns the current executable's absolute path.
|
||||
func launcherPath() (string, error) {
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Abs(exe)
|
||||
}
|
||||
|
||||
func install() error {
|
||||
srcPath, err := launcherPath()
|
||||
if err != nil {
|
||||
return fmt.Errorf("실행 파일 경로를 찾을 수 없습니다: %w", err)
|
||||
}
|
||||
|
||||
dir, err := installDir()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return fmt.Errorf("설치 디렉토리 생성 실패: %w", err)
|
||||
}
|
||||
|
||||
dstPath := filepath.Join(dir, "launcher.exe")
|
||||
|
||||
if !strings.EqualFold(srcPath, dstPath) {
|
||||
if err := copyFile(srcPath, dstPath); err != nil {
|
||||
return fmt.Errorf("런처 설치 실패: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
key, _, err := registry.CreateKey(registry.CURRENT_USER, `Software\Classes\`+protocolName, registry.SET_VALUE)
|
||||
if err != nil {
|
||||
return fmt.Errorf("레지스트리 키 생성 실패: %w", err)
|
||||
}
|
||||
defer key.Close()
|
||||
if err := key.SetStringValue("", "URL:One of the plans Protocol"); err != nil {
|
||||
return fmt.Errorf("프로토콜 값 설정 실패: %w", err)
|
||||
}
|
||||
if err := key.SetStringValue("URL Protocol", ""); err != nil {
|
||||
return fmt.Errorf("URL Protocol 값 설정 실패: %w", err)
|
||||
}
|
||||
|
||||
cmdKey, _, err := registry.CreateKey(registry.CURRENT_USER, `Software\Classes\`+protocolName+`\shell\open\command`, registry.SET_VALUE)
|
||||
if err != nil {
|
||||
return fmt.Errorf("command 키 생성 실패: %w", err)
|
||||
}
|
||||
defer cmdKey.Close()
|
||||
return cmdKey.SetStringValue("", fmt.Sprintf(`"%s" "%%1"`, dstPath))
|
||||
}
|
||||
|
||||
func uninstall() error {
|
||||
paths := []string{
|
||||
`Software\Classes\` + protocolName + `\shell\open\command`,
|
||||
`Software\Classes\` + protocolName + `\shell\open`,
|
||||
`Software\Classes\` + protocolName + `\shell`,
|
||||
`Software\Classes\` + protocolName,
|
||||
}
|
||||
for _, p := range paths {
|
||||
if err := registry.DeleteKey(registry.CURRENT_USER, p); err != nil && err != registry.ErrNotExist {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func fetchServerInfoOnce() (*downloadInfo, error) {
|
||||
resp, err := apiClient.Get(serverInfoURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("서버 연결 실패: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == 404 {
|
||||
return nil, &errNoRetry{fmt.Errorf("게임이 아직 준비되지 않았습니다")}
|
||||
}
|
||||
if resp.StatusCode >= 400 {
|
||||
return nil, &errNoRetry{fmt.Errorf("서버 오류 (HTTP %d)", resp.StatusCode)}
|
||||
}
|
||||
|
||||
var info downloadInfo
|
||||
if err := json.NewDecoder(io.LimitReader(resp.Body, 1<<20)).Decode(&info); err != nil {
|
||||
return nil, fmt.Errorf("서버 응답 파싱 실패: %w", err)
|
||||
}
|
||||
return &info, nil
|
||||
}
|
||||
|
||||
func fetchServerInfo() (*downloadInfo, error) {
|
||||
const maxRetries = 3
|
||||
var lastErr error
|
||||
for i := range maxRetries {
|
||||
info, err := fetchServerInfoOnce()
|
||||
if err == nil {
|
||||
return info, nil
|
||||
}
|
||||
lastErr = err
|
||||
var noRetry *errNoRetry
|
||||
if errors.As(err, &noRetry) {
|
||||
return nil, err
|
||||
}
|
||||
time.Sleep(time.Duration(1<<i) * time.Second)
|
||||
}
|
||||
return nil, fmt.Errorf("서버 연결 실패 (%d회 재시도): %w", maxRetries, lastErr)
|
||||
}
|
||||
|
||||
// redeemTicket exchanges a one-time launch ticket for a fresh JWT access token.
|
||||
func redeemTicket(ticket string) (string, error) {
|
||||
return redeemTicketFrom(redeemTicketURL, ticket)
|
||||
}
|
||||
|
||||
func redeemTicketFrom(url, ticket string) (string, error) {
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
body := fmt.Sprintf(`{"ticket":"%s"}`, ticket)
|
||||
resp, err := client.Post(url, "application/json", strings.NewReader(body))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("서버에 연결할 수 없습니다: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("런처 인증에 실패했습니다 (HTTP %d)", resp.StatusCode)
|
||||
}
|
||||
|
||||
var result struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
if err := json.NewDecoder(io.LimitReader(resp.Body, 1<<20)).Decode(&result); err != nil {
|
||||
return "", fmt.Errorf("서버 응답을 처리할 수 없습니다: %w", err)
|
||||
}
|
||||
if result.Token == "" {
|
||||
return "", fmt.Errorf("서버가 토큰을 반환하지 않았습니다")
|
||||
}
|
||||
return result.Token, nil
|
||||
}
|
||||
Reference in New Issue
Block a user