package main import ( "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "io" "net/http" "net/url" "os" "os/exec" "path/filepath" "strings" "unsafe" "golang.org/x/sys/windows" "golang.org/x/sys/windows/registry" ) const ( protocolName = "a301" gameExeName = "A301.exe" serverInfoURL = "https://a301.tolelom.xyz/api/download/info" webDownloadURL = "https://a301.tolelom.xyz" ) var ( user32 = windows.NewLazySystemDLL("user32.dll") messageBoxW = user32.NewProc("MessageBoxW") shellExecuteW = windows.NewLazySystemDLL("shell32.dll").NewProc("ShellExecuteW") ) const ( mbOK = 0x00000000 mbIconInfo = 0x00000040 mbIconError = 0x00000010 mbYesNo = 0x00000004 mbIconQuestion = 0x00000020 idYes = 6 ) func msgBox(title, text string, flags uintptr) int { t, _ := windows.UTF16PtrFromString(title) m, _ := windows.UTF16PtrFromString(text) ret, _, _ := messageBoxW.Call( 0, uintptr(unsafe.Pointer(m)), uintptr(unsafe.Pointer(t)), flags, ) return int(ret) } func openBrowser(rawURL string) { u, _ := windows.UTF16PtrFromString(rawURL) op, _ := windows.UTF16PtrFromString("open") shellExecuteW.Call(0, uintptr(unsafe.Pointer(op)), uintptr(unsafe.Pointer(u)), 0, 0, 1) } func main() { if len(os.Args) < 2 { ret := msgBox( "A301 런처", "게임 실행을 위해 프로토콜을 등록합니다.\n계속하시겠습니까?", mbYesNo|mbIconQuestion, ) if ret != idYes { return } if err := install(); err != nil { msgBox("A301 런처 - 오류", fmt.Sprintf("등록 실패:\n%v", err), mbOK|mbIconError) os.Exit(1) } msgBox("A301 런처", "a301:// 프로토콜이 등록되었습니다.\n이제 웹에서 게임 시작 버튼을 사용할 수 있습니다.", mbOK|mbIconInfo) return } arg := os.Args[1] switch { case arg == "install": if err := install(); err != nil { msgBox("A301 런처 - 오류", fmt.Sprintf("등록 실패:\n%v", err), mbOK|mbIconError) os.Exit(1) } msgBox("A301 런처", "a301:// 프로토콜이 등록되었습니다.", mbOK|mbIconInfo) case arg == "uninstall": if err := uninstall(); err != nil { msgBox("A301 런처 - 오류", fmt.Sprintf("제거 실패:\n%v", err), mbOK|mbIconError) os.Exit(1) } msgBox("A301 런처", "a301:// 프로토콜이 제거되었습니다.", mbOK|mbIconInfo) case strings.HasPrefix(arg, protocolName+"://"): if err := handleURI(arg); err != nil { msgBox("A301 런처 - 오류", fmt.Sprintf("실행 실패:\n%v", err), mbOK|mbIconError) os.Exit(1) } default: msgBox("A301 런처 - 오류", fmt.Sprintf("알 수 없는 명령: %s", arg), mbOK|mbIconError) os.Exit(1) } } func launcherPath() (string, error) { exe, err := os.Executable() if err != nil { return "", err } return filepath.Abs(exe) } func install() error { exePath, err := launcherPath() if 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:A301 Protocol"); err != nil { return err } if err := key.SetStringValue("URL Protocol", ""); err != nil { return 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() cmdValue := fmt.Sprintf(`"%s" "%%1"`, exePath) return cmdKey.SetStringValue("", cmdValue) } 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 { err := registry.DeleteKey(registry.CURRENT_USER, p) if err != nil && err != registry.ErrNotExist { return err } } return nil } type downloadInfo struct { FileHash string `json:"fileHash"` URL string `json:"url"` } func fetchServerInfo() (*downloadInfo, error) { resp, err := http.Get(serverInfoURL) if err != nil { return nil, fmt.Errorf("서버 연결 실패: %w", err) } defer resp.Body.Close() var info downloadInfo if err := json.NewDecoder(resp.Body).Decode(&info); err != nil { return nil, fmt.Errorf("서버 응답 파싱 실패: %w", err) } return &info, nil } func hashFile(path string) (string, error) { f, err := os.Open(path) if err != nil { return "", err } defer f.Close() h := sha256.New() if _, err := io.Copy(h, f); err != nil { return "", err } return hex.EncodeToString(h.Sum(nil)), nil } func handleURI(rawURI string) error { parsed, err := url.Parse(rawURI) if err != nil { return fmt.Errorf("URI 파싱 실패: %w", err) } token := parsed.Query().Get("token") if token == "" { return fmt.Errorf("토큰이 없습니다") } exePath, err := launcherPath() if err != nil { return err } gameDir := filepath.Dir(exePath) gamePath := filepath.Join(gameDir, gameExeName) if _, err := os.Stat(gamePath); os.IsNotExist(err) { return fmt.Errorf("게임 파일을 찾을 수 없습니다:\n%s", gamePath) } // 서버에서 최신 해시 조회 serverInfo, err := fetchServerInfo() if err != nil { return fmt.Errorf("버전 확인 실패:\n%w", err) } // 서버에 해시가 등록된 경우에만 검증 if serverInfo.FileHash != "" { localHash, err := hashFile(gamePath) if err != nil { return fmt.Errorf("게임 파일 검증 실패:\n%w", err) } if !strings.EqualFold(localHash, serverInfo.FileHash) { ret := msgBox( "A301 - 업데이트 필요", "새로운 버전의 게임이 있습니다.\n최신 버전을 다운로드해주세요.\n\n확인을 누르면 다운로드 페이지로 이동합니다.", mbOK|mbIconInfo, ) if ret > 0 { openBrowser(webDownloadURL) } return fmt.Errorf("버전이 최신이 아닙니다") } } cmd := exec.Command(gamePath, "-token", token) cmd.Dir = gameDir if err := cmd.Start(); err != nil { return fmt.Errorf("게임 실행 실패: %w", err) } return nil }