1
0
mirror of https://github.com/zu1k/nali.git synced 2025-01-22 21:29:02 +08:00
nali/internal/repo/update.go

184 lines
5.4 KiB
Go

package repo
import (
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"log"
"os"
"path/filepath"
"github.com/zu1k/nali/internal/constant"
"github.com/google/go-github/v55/github"
)
var (
ctx = context.Background()
tAsset *github.ReleaseAsset
shaAsset *github.ReleaseAsset
)
func UpdateRepo() error {
rel, err := getLatestRelease()
if err != nil {
return fmt.Errorf("failed to get latest release: %v", err)
}
if !canUpdate(rel) {
log.Printf("current version is already the latest version, no update \n")
return nil
}
//Filtering assets by GOOS and GOARCH
if tAsset = getTargetAsset(rel, false); tAsset == nil {
return fmt.Errorf("no target asset found for %s %s", constant.OS, constant.Arch)
}
if shaAsset = getTargetAsset(rel, true); shaAsset == nil {
return fmt.Errorf("no sha256 asset found for %s %s", constant.OS, constant.Arch)
}
//Download the new version nali and its sha256
data, err := download(ctx, tAsset.GetID())
if err != nil {
return fmt.Errorf("failed to download asset %v: %v", tAsset.GetID(), err)
}
vData, err := download(ctx, shaAsset.GetID())
if err != nil {
return fmt.Errorf("failed to download asset %v: %v", tAsset.GetID(), err)
}
// Verifying files with sha256
vHash := make([]byte, sha256.Size)
if _, err := hex.Decode(vHash, vData[:sha256.BlockSize]); err != nil {
return fmt.Errorf("failed to decode sha256 hash: %v", err)
}
if !validate(data, vHash) {
return fmt.Errorf("failed to validate asset %v, sha256 check failed", tAsset.GetID())
}
// Unzip and replace nali itself
exe, err := os.Executable()
if err != nil {
return fmt.Errorf("could not locate executable path: %v", err)
}
asset, err := decompress(bytes.NewReader(data), tAsset.GetName())
if err != nil {
return fmt.Errorf("error occurred while decompress: %v", err)
}
log.Printf("Updating %v to %v \n", exe, rel.GetTagName())
if err = update(asset, exe); err != nil {
return fmt.Errorf("update executable failed: %v", err)
}
log.Printf("Successfully updated to version %v \n", rel.GetTagName())
return nil
}
func update(asset io.Reader, cmdPath string) error {
newBytes, err := io.ReadAll(asset)
if err != nil {
return err
}
// get the directory the executable exists in
updateDir := filepath.Dir(cmdPath)
filename := filepath.Base(cmdPath)
// some of our users may install nali through package management, we need to check the permissions before updating
if !canWriteDir(updateDir) {
return fmt.Errorf("no write permissions on the directory, consider updating nali manually")
}
// Copy the contents of new binary to a new executable file
newPath := filepath.Join(updateDir, fmt.Sprintf(".%s.new", filename))
fp, err := os.OpenFile(newPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0755)
if err != nil {
_ = fp.Close()
return fmt.Errorf("create the new executable file failed: %v", err)
}
if _, err = io.Copy(fp, bytes.NewReader(newBytes)); err != nil {
_ = fp.Close()
return fmt.Errorf("copy the new executable file failed: %v", err)
}
// if we don't call fp.Close(), windows won't let us move the new executable
// because the file will still be "in use"
if err = fp.Close(); err != nil {
return fmt.Errorf("failed to close file, may cause file corruption, nali updation was cancelled: %v", err)
}
oldPath := filepath.Join(updateDir, fmt.Sprintf(".%s.old", filename))
// delete any existing old exec file - this is necessary on Windows for two reasons:
// 1. after a successful asset, Windows can't remove the .old file because the process is still running
// 2. windows rename operations fail if the destination file already exists
_ = os.Remove(oldPath)
if err = os.Rename(cmdPath, oldPath); err != nil {
return fmt.Errorf("rename the old executable file failed: %v", err)
}
if err = os.Rename(newPath, cmdPath); err != nil {
// move unsuccessful
// The filesystem is now in a bad state. We have successfully
// moved the existing binary to a new location, but we couldn't move the new
// binary to take its place. That means there is no file where the current executable binary
// used to be!
// Try to rollback by restoring the old binary to its original path.
if rerr := os.Rename(oldPath, cmdPath); rerr != nil {
return fmt.Errorf("unable to rollback binary: %v", rerr)
}
return fmt.Errorf("unable to move new binary to executable path: %v", err)
}
if err = os.Remove(oldPath); err != nil {
// windows has trouble with removing old binaries, so do nothing only print log
log.Printf("remove old binary failed, please remove the old binary manually: %v \n", err)
}
return nil
}
func canUpdate(rel *github.RepositoryRelease) bool {
// unknown version means that the user compiled it manually instead of downloading it from the release,
// in which case we don't take the liberty of updating it to a potentially older version.
if constant.Version == "unknown version" {
return false
}
latest, err := parseVersion(rel.GetTagName())
if err != nil {
log.Printf("failed to parse latest version: %v, err: %v \n", rel.GetTagName(), err)
return false
}
cur, err := parseVersion(constant.Version)
if err != nil {
log.Printf("failed to parse current version: %v, err: %v \n", constant.Version, err)
return false
}
return latest.GreaterThan(cur)
}
func canWriteDir(path string) bool {
fp := filepath.Join(path, ".tempWriteCheck")
defer os.Remove(fp)
file, err := os.Create(fp)
if err == nil {
file.Close()
}
return err == nil
}