- 서버 /api/download/info에서 최신 fileHash 조회 - 로컬 A301.exe SHA256과 비교 - 불일치 시 게임 실행 차단 + 다운로드 페이지 열기 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
258 lines
6.2 KiB
Go
258 lines
6.2 KiB
Go
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
|
|
}
|