package agent import ( "context" "crypto/sha256" "encoding/hex" "errors" "fmt" "io" "os" "os/exec" "path/filepath" "strings" "time" "gitea.hsrv.site/mrleo1nid/backup-master/internal/api" ) // updatedEnv выставляется перезапущенному процессу: не обновляться повторно в том же запуске. const updatedEnv = "BM_UPDATED_FROM" // selfUpdate скачивает новую версию агента с того же сервера, сверяет sha256, // подменяет бинарник (старый сохраняется для отката) и перезапускает процесс. // При успехе не возвращается. func selfUpdate(ctx context.Context, client *api.Client, u *api.AgentUpdate, current string) error { // только путь на своём сервере: чужой адрес от сервера не принимаем if !strings.HasPrefix(u.Path, "/dl/") || strings.Contains(u.Path, "..") || strings.Contains(u.Path, "://") { return fmt.Errorf("недопустимый путь обновления %q", u.Path) } if len(u.SHA256) != 64 { return errors.New("сервер не прислал sha256") } exe, err := os.Executable() if err != nil { return err } if exe, err = filepath.EvalSymlinks(exe); err != nil { return err } dctx, cancel := context.WithTimeout(ctx, 5*time.Minute) defer cancel() body, err := client.Download(dctx, u.Path) if err != nil { return fmt.Errorf("скачивание: %w", err) } defer body.Close() tmp := exe + ".new" if err := writeVerified(tmp, body, u.SHA256); err != nil { os.Remove(tmp) return err } // новый бинарник должен запускаться и сообщать ожидаемую версию if err := checkVersion(ctx, tmp, u.Version); err != nil { os.Remove(tmp) return err } old := exe + ".old" if err := os.Rename(exe, old); err != nil { os.Remove(tmp) return err } if err := os.Rename(tmp, exe); err != nil { _ = os.Rename(old, exe) return err } env := append(os.Environ(), updatedEnv+"="+current) err = reexec(exe, os.Args, env) // сюда попадаем, только если exec не удался — возвращаем старую версию _ = os.Rename(old, exe) return fmt.Errorf("перезапуск: %w", err) } func writeVerified(path string, r io.Reader, want string) error { f, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o755) if err != nil { return err } h := sha256.New() if _, err := io.Copy(io.MultiWriter(f, h), r); err != nil { f.Close() return err } if err := f.Close(); err != nil { return err } if got := hex.EncodeToString(h.Sum(nil)); got != want { return fmt.Errorf("sha256 не совпал: %s, ожидался %s", got, want) } return os.Chmod(path, 0o755) } func checkVersion(ctx context.Context, bin, want string) error { cctx, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() out, err := exec.CommandContext(cctx, bin, "version").Output() if err != nil { return fmt.Errorf("новый бинарник не запускается: %w", err) } if !strings.Contains(string(out), want) { return fmt.Errorf("новый бинарник сообщает версию %q, ожидалась %s", strings.TrimSpace(string(out)), want) } return nil }