package gitService import ( "crypto/sha256" "crypto/subtle" "encoding/hex" "encoding/json" "fmt" "io" "net/http" "net/url" "os" "os/exec" "path/filepath" "regexp" "strings" "time" "g-launcher/pkg/constant" "g-launcher/pkg/models" "github.com/minio/selfupdate" ) type GitService struct{} type launcherManifest struct { Version string `json:"version"` SHA256 string `json:"sha256"` } var launcherVersionPattern = regexp.MustCompile(`^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$`) func (g *GitService) GetLatestLauncherVersion() (bool, string, string) { // First, attempt to check via latest.json manifest if constant.LauncherManifestURL != "" { manifest, err := fetchLauncherManifest(constant.LauncherManifestURL) if err == nil && manifest.Version != "" { return true, manifest.Version, "" } } client := &http.Client{Timeout: 10 * time.Second} // Fallback to Gitea Releases API if constant.LauncherGitUrl != "" { resp, err := client.Get(constant.LauncherGitUrl) if err == nil { defer resp.Body.Close() body, readErr := io.ReadAll(resp.Body) if readErr == nil { var releases []models.ReleaseType if json.Unmarshal(body, &releases) == nil && len(releases) > 0 { return true, releases[0].TagName, "" } } } } // Fallback to Gitea Tags API tagsURL := "https://gaytea.cod-algieba.ts.net/api/v1/repos/GaCy/G-Launcher/tags" resp, err := client.Get(tagsURL) if err == nil { defer resp.Body.Close() body, readErr := io.ReadAll(resp.Body) if readErr == nil { var tags []struct { Name string `json:"name"` } if json.Unmarshal(body, &tags) == nil && len(tags) > 0 { return true, tags[0].Name, "" } } } return true, constant.CurrentLauncherVersion, "" } func (g *GitService) UpdateLauncherProgress(version string) (bool, string) { if !constant.LauncherUpdatesEnabled { return false, "launcher updates are disabled in development builds" } // Mode 1: Manifest-based update with SHA-256 integrity verification if constant.LauncherManifestURL != "" && constant.LauncherDownloadURL != "" { manifest, err := fetchLauncherManifest(constant.LauncherManifestURL) if err == nil && manifest.Version == version && manifest.SHA256 != "" { return g.applyManifestUpdate(manifest) } } // Mode 2: Release API fallback if constant.LauncherGitUrl != "" { asset, ok := g.getReleaseAsset(version, constant.LauncherGitUrl, constant.LauncherFile) if ok && asset.BrowserDownloadURL != "" { resp, err := http.Get(asset.BrowserDownloadURL) if err != nil { return false, err.Error() } defer resp.Body.Close() err = selfupdate.Apply(resp.Body, selfupdate.Options{}) if err != nil { return false, err.Error() } return true, "" } } // Mode 3: Build from source inside a temporary workspace, replace binary, and delete temp return g.BuildAndUpdateFromSource(constant.LauncherBranch) } func (g *GitService) BuildAndUpdateFromSource(branch string) (bool, string) { if branch == "" { branch = constant.LauncherBranch } gitExe, err := ensureGit() if err != nil { return false, fmt.Sprintf("git is required for source update: %v", err) } // 1. Create clean temporary directory tempDir, err := os.MkdirTemp("", "g-launcher-build-*") if err != nil { return false, fmt.Sprintf("create temp build directory: %v", err) } defer os.RemoveAll(tempDir) // Guarantee temporary directory is completely deleted // 2. Clone repository into temp directory cloneCmd := exec.Command(gitExe, "clone", "--depth", "1", "--branch", branch, constant.LauncherRepoURL, tempDir) cloneCmd.Env = append(os.Environ(), "GIT_TERMINAL_PROMPT=0") if out, err := cloneCmd.CombinedOutput(); err != nil { return false, fmt.Sprintf("clone repository to temp: %v (%s)", err, strings.TrimSpace(string(out))) } // 3. Build frontend inside temp frontendDir := filepath.Join(tempDir, "frontend") npmInstall := exec.Command("npm", "install", "--no-audit", "--no-fund") npmInstall.Dir = frontendDir if out, err := npmInstall.CombinedOutput(); err != nil { return false, fmt.Sprintf("npm install in temp: %v (%s)", err, strings.TrimSpace(string(out))) } npmBuild := exec.Command("npm", "run", "build") npmBuild.Dir = frontendDir if out, err := npmBuild.CombinedOutput(); err != nil { return false, fmt.Sprintf("npm run build in temp: %v (%s)", err, strings.TrimSpace(string(out))) } // 4. Build Go binary inside temp builtExePath := filepath.Join(tempDir, "bin", "G-Launcher.exe") if err := os.MkdirAll(filepath.Dir(builtExePath), 0755); err != nil { return false, fmt.Sprintf("create temp bin directory: %v", err) } goBuild := exec.Command("go", "build", "-tags", "production", "-trimpath", "-buildvcs=false", "-ldflags=-w -s -H windowsgui", "-o", builtExePath, ".") goBuild.Dir = tempDir if out, err := goBuild.CombinedOutput(); err != nil { return false, fmt.Sprintf("go build in temp: %v (%s)", err, strings.TrimSpace(string(out))) } // 5. Replace current running binary with newly built binary builtFile, err := os.Open(builtExePath) if err != nil { return false, fmt.Sprintf("open built launcher in temp: %v", err) } defer builtFile.Close() if err := selfupdate.Apply(builtFile, selfupdate.Options{}); err != nil { return false, fmt.Sprintf("apply launcher update: %v", err) } return true, "" } func (g *GitService) applyManifestUpdate(manifest launcherManifest) (bool, string) { client := &http.Client{Timeout: 10 * time.Minute} response, err := client.Get(constant.LauncherDownloadURL) if err != nil { return false, fmt.Errorf("download launcher update: %w", err).Error() } defer response.Body.Close() if response.StatusCode < http.StatusOK || response.StatusCode >= http.StatusMultipleChoices { return false, fmt.Sprintf("download launcher update: server returned %s", response.Status) } temporaryFile, err := os.CreateTemp("", "g-launcher-update-*.exe") if err != nil { return false, fmt.Errorf("create temporary launcher update: %w", err).Error() } temporaryPath := temporaryFile.Name() defer os.Remove(temporaryPath) hash := sha256.New() if _, err := io.Copy(io.MultiWriter(temporaryFile, hash), response.Body); err != nil { temporaryFile.Close() return false, fmt.Errorf("save launcher update: %w", err).Error() } if err := temporaryFile.Close(); err != nil { return false, fmt.Errorf("close launcher update: %w", err).Error() } expectedHash, err := hex.DecodeString(manifest.SHA256) if err != nil || subtle.ConstantTimeCompare(hash.Sum(nil), expectedHash) != 1 { return false, "launcher update checksum mismatch" } updateFile, err := os.Open(temporaryPath) if err != nil { return false, fmt.Errorf("open launcher update: %w", err).Error() } defer updateFile.Close() if err := selfupdate.Apply(updateFile, selfupdate.Options{}); err != nil { return false, fmt.Errorf("apply launcher update: %w", err).Error() } return true, "" } func fetchLauncherManifest(manifestURL string) (launcherManifest, error) { parsedURL, err := url.Parse(manifestURL) if err != nil || (parsedURL.Scheme != "https" && parsedURL.Scheme != "http") { return launcherManifest{}, fmt.Errorf("invalid launcher manifest URL") } query := parsedURL.Query() query.Set("_", fmt.Sprintf("%d", time.Now().UnixNano())) parsedURL.RawQuery = query.Encode() request, err := http.NewRequest(http.MethodGet, parsedURL.String(), nil) if err != nil { return launcherManifest{}, fmt.Errorf("create launcher manifest request: %w", err) } request.Header.Set("Cache-Control", "no-cache") request.Header.Set("User-Agent", "G-Launcher") client := &http.Client{Timeout: 20 * time.Second} response, err := client.Do(request) if err != nil { return launcherManifest{}, fmt.Errorf("check launcher updates: %w", err) } defer response.Body.Close() if response.StatusCode < http.StatusOK || response.StatusCode >= http.StatusMultipleChoices { return launcherManifest{}, fmt.Errorf("check launcher updates: server returned %s", response.Status) } var manifest launcherManifest if err := json.NewDecoder(io.LimitReader(response.Body, 1<<20)).Decode(&manifest); err != nil { return launcherManifest{}, fmt.Errorf("read launcher update manifest: %w", err) } if !launcherVersionPattern.MatchString(manifest.Version) { return launcherManifest{}, fmt.Errorf("launcher update manifest has invalid version %q", manifest.Version) } manifest.SHA256 = strings.TrimSpace(manifest.SHA256) if len(manifest.SHA256) != sha256.Size*2 { return launcherManifest{}, fmt.Errorf("launcher update manifest has invalid SHA-256") } if _, err := hex.DecodeString(manifest.SHA256); err != nil { return launcherManifest{}, fmt.Errorf("launcher update manifest has invalid SHA-256: %w", err) } manifest.SHA256 = strings.ToLower(manifest.SHA256) return manifest, nil }