package main import ( "fmt" "net/url" "os" "os/exec" "path/filepath" "strings" ) // version is set at build time via -ldflags "-X main.version=x.y.z" var version = "dev" func handleURI(rawURI string) error { parsed, err := url.Parse(rawURI) if err != nil { return fmt.Errorf("URI 파싱 실패: %w", err) } if parsed.Scheme != protocolName { return fmt.Errorf("지원하지 않는 URI 스킴: %s", parsed.Scheme) } // 웹 클라이언트가 발급한 일회용 티켓을 서버에서 JWT로 교환 ticket := parsed.Query().Get("token") if ticket == "" { return fmt.Errorf("토큰이 없습니다") } token, err := redeemTicket(ticket) if err != nil { return fmt.Errorf("런처 인증에 실패했습니다: %w", err) } // JWT는 점(.)으로 구분된 3파트 형식이어야 함 if parts := strings.Split(token, "."); len(parts) != 3 { return fmt.Errorf("서버에서 유효하지 않은 토큰을 받았습니다") } gameDir, err := installDir() if err != nil { return err } if err := os.MkdirAll(gameDir, 0755); err != nil { return fmt.Errorf("게임 디렉토리 생성 실패: %w", err) } gamePath := filepath.Join(gameDir, gameExeName) // 프로토콜 등록이 현재 런처를 가리키도록 갱신 (사일런트) _ = install() // 이전 업데이트에서 남은 .old/.new 파일 정리 cleanupOldFiles(gameDir) serverInfo, err := fetchServerInfo() if err != nil { // 오프라인 모드: 게임이 이미 설치되어 있으면 직접 실행 if _, statErr := os.Stat(gamePath); statErr == nil { ret := msgBox("One of the plans", "서버에 연결할 수 없습니다.\n설치된 게임을 실행하시겠습니까?\n(업데이트 확인 불가)", mbYesNo|mbQ) if ret == idYes { cmd := exec.Command(gamePath, "-token", token) cmd.Dir = gameDir if err := cmd.Start(); err != nil { return fmt.Errorf("게임 실행 실패: %w", err) } return nil } return fmt.Errorf("사용자가 취소했습니다") } return fmt.Errorf("버전 확인 실패: %w", err) } // 런처 자동 업데이트 체크 if updated, updateErr := ensureLauncher(serverInfo); updateErr != nil { fmt.Fprintf(os.Stderr, "런처 업데이트 실패: %v\n", updateErr) } else if updated { cmd := exec.Command(os.Args[0], os.Args[1:]...) if err := cmd.Start(); err != nil { return fmt.Errorf("새 런처 시작 실패: %w", err) } os.Exit(0) } if err := ensureGame(gameDir, gamePath, serverInfo); err != nil { return err } cmd := exec.Command(gamePath, "-token", token) cmd.Dir = gameDir if err := cmd.Start(); err != nil { return fmt.Errorf("게임 실행 실패: %w", err) } return nil } func main() { // DLL Hijacking 방어: 시스템 디렉토리에서만 DLL 로드 const loadLibrarySearchSystem32 = 0x00000800 kernel32.NewProc("SetDefaultDllDirectories").Call(loadLibrarySearchSystem32) enableDPIAwareness() if !acquireSingleInstance() { activateExistingWindow() return } if len(os.Args) < 2 { if err := install(); err != nil { msgBox("One of the plans 런처 - 오류", fmt.Sprintf("설치 실패:\n%v", err), mbOK|mbError) os.Exit(1) } msgBox("One of the plans", "설치가 완료되었습니다.\n웹에서 게임 시작 버튼을 클릭하세요.", mbOK|mbInfo) return } arg := os.Args[1] switch { case arg == "install": if err := install(); err != nil { msgBox("One of the plans 런처 - 오류", fmt.Sprintf("등록 실패:\n%v", err), mbOK|mbError) os.Exit(1) } msgBox("One of the plans 런처", "a301:// 프로토콜이 등록되었습니다.", mbOK|mbInfo) case arg == "uninstall": ret := msgBox("One of the plans 런처", "게임 데이터도 함께 삭제하시겠습니까?", mbYesNo|mbQ) deleteData := ret == idYes if err := uninstall(); err != nil { msgBox("One of the plans 런처 - 오류", fmt.Sprintf("제거 실패:\n%v", err), mbOK|mbError) os.Exit(1) } if deleteData { if dir, err := installDir(); err == nil { os.RemoveAll(dir) } } msgBox("One of the plans 런처", "a301:// 프로토콜이 제거되었습니다.", mbOK|mbInfo) case arg == "--version" || arg == "version": msgBox("One of the plans 런처", fmt.Sprintf("버전: %s", version), mbOK|mbInfo) case strings.HasPrefix(arg, protocolName+"://"): if err := handleURI(arg); err != nil { msgBox("One of the plans 런처 - 오류", fmt.Sprintf("실행 실패:\n%v", err), mbOK|mbError) os.Exit(1) } default: msgBox("One of the plans 런처 - 오류", fmt.Sprintf("알 수 없는 명령: %s", arg), mbOK|mbError) os.Exit(1) } }