Files
SamWaf/wafupdate/selfupdate.go
T
2026-05-11 16:58:52 +08:00

861 lines
22 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package wafupdate
import (
"SamWaf/binarydist"
"SamWaf/global"
"SamWaf/utils"
"bytes"
"compress/gzip"
"crypto/sha256"
"encoding/json"
"errors"
"fmt"
"golang.org/x/mod/semver"
"io"
"io/ioutil"
"log"
"math/rand"
"net/url"
"os"
"path/filepath"
"runtime"
"sort"
"strings"
"time"
)
const (
// holds a timestamp which triggers the next update
upcktimePath = "cktime" // path to timestamp file relative to u.Dir
plat = runtime.GOOS + "-" + runtime.GOARCH // ex: linux-amd64
)
var (
ErrHashMismatch = errors.New("new file hash mismatch after patch")
defaultHTTPRequester = HTTPRequester{}
)
// Updater is the configuration and runtime data for doing an update.
//
// Note that ApiURL, BinURL and DiffURL should have the same value if all files are available at the same location.
//
// Example:
//
// updater := &selfupdate.Updater{
// CurrentVersion: version,
// ApiURL: "http://updates.yourdomain.com/",
// BinURL: "http://updates.yourdownmain.com/",
// DiffURL: "http://updates.yourdomain.com/",
// Dir: "update/",
// CmdName: "myapp", // app name
// }
// if updater != nil {
// go updater.BackgroundRun()
// }
type Updater struct {
CurrentVersion string // Currently running version. `dev` is a special version here and will cause the updater to never update.
ApiURL string // Base URL for API requests (JSON files).
CmdName string // Command name is appended to the ApiURL like http://apiurl/CmdName/. This represents one binary.
BinURL string // Base URL for full binary downloads.
DiffURL string // Base URL for diff downloads.
BinGithubURL string // Base URL for full binary downloads.
Dir string // Directory to store selfupdate state.
ForceCheck bool // Check for update regardless of cktime timestamp
CheckTime int // Time in hours before next check
RandomizeTime int // Time in hours to randomize with CheckTime
Requester Requester // Optional parameter to override existing HTTP request handler
Info struct {
Version string
Sha256 []byte
Desc string
}
OnSuccessfulUpdate func() // Optional function to run after an update has successfully taken place
}
func (u *Updater) getExecRelativeDir(dir string) string {
filename, _ := os.Executable()
path := filepath.Join(filepath.Dir(filename), dir)
return path
}
func canUpdate() (err error) {
// get the directory the file exists in
path, err := os.Executable()
if err != nil {
return
}
fileDir := filepath.Dir(path)
fileName := filepath.Base(path)
// attempt to open a file in the file's directory
newPath := filepath.Join(fileDir, fmt.Sprintf(".%s.new", fileName))
fp, err := os.OpenFile(newPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0755)
if err != nil {
return
}
fp.Close()
_ = os.Remove(newPath)
return
}
// BackgroundRun starts the update check and apply cycle.
func (u *Updater) BackgroundRun() error {
return u.BackgroundRunWithChannel("official")
}
func (u *Updater) BackgroundRunWithChannel(channel string) error {
if channel == "" || channel == "official" {
if err := os.MkdirAll(u.getExecRelativeDir(u.Dir), 0755); err != nil {
// fail
return err
}
// check to see if we want to check for updates based on version
// and last update time
if u.WantUpdate() {
if err := canUpdate(); err != nil {
// fail
return err
}
u.SetUpdateTime()
if err := u.Update(); err != nil {
return err
}
}
} else if channel == "github" {
if err := os.MkdirAll(u.getExecRelativeDir(u.Dir), 0755); err != nil {
// fail
return err
}
if err := canUpdate(); err != nil {
// fail
return err
}
u.SetUpdateTime()
if err := u.UpdateWithChannel(channel); err != nil {
return err
}
}
return nil
}
// WantUpdate returns boolean designating if an update is desired. If the app's version
// is `dev` WantUpdate will return false. If u.ForceCheck is true or cktime is after now
// WantUpdate will return true.
func (u *Updater) WantUpdate() bool {
if u.CurrentVersion == "dev" || (!u.ForceCheck && u.NextUpdate().After(time.Now())) {
return false
}
return true
}
// NextUpdate returns the next time update should be checked
func (u *Updater) NextUpdate() time.Time {
path := u.getExecRelativeDir(u.Dir + upcktimePath)
nextTime := readTime(path)
return nextTime
}
// SetUpdateTime writes the next update time to the state file
func (u *Updater) SetUpdateTime() bool {
path := u.getExecRelativeDir(u.Dir + upcktimePath)
wait := time.Duration(u.CheckTime) * time.Hour
// Add 1 to random time since max is not included
waitrand := time.Duration(rand.Intn(u.RandomizeTime+1)) * time.Hour
return writeTime(path, time.Now().Add(wait+waitrand))
}
// ClearUpdateState writes current time to state file
func (u *Updater) ClearUpdateState() {
path := u.getExecRelativeDir(u.Dir + upcktimePath)
os.Remove(path)
}
func (u *Updater) UpdateAvailableWithChannel(channel string) (bool, string, string, error) {
path, err := os.Executable()
if err != nil {
return false, "", "", err
}
old, err := os.Open(path)
if err != nil {
return false, "", "", err
}
defer old.Close()
//渠道选择
if channel == "" || channel == "official" {
err = u.fetchInfo()
if err != nil {
return false, "", "", err
}
// 比较版本号
cmp := semver.Compare(u.Info.Version, u.CurrentVersion)
// 如果更新的版本大于当前版本,返回 true,表示有可用的更新
if cmp > 0 {
return true, u.Info.Version, u.Info.Desc, nil
} else {
// 否则,返回 false,表示没有可用的更新
return false, "", "", nil
}
} else if channel == "github" {
err = u.fetchInfoGithub()
if err != nil {
return false, "", "", err
}
// 比较版本号
cmp := semver.Compare(u.Info.Version, u.CurrentVersion)
// 如果更新的版本大于当前版本,返回 true,表示有可用的更新
if cmp > 0 {
return true, u.Info.Version, u.Info.Desc, nil
} else {
// 否则,返回 false,表示没有可用的更新
return false, "", "", nil
}
} else {
// 否则,返回 false,表示没有可用的更新
return false, "", "", nil
}
}
// UpdateAvailable 默认从官方下载
func (u *Updater) UpdateAvailable() (bool, string, string, error) {
return u.UpdateAvailableWithChannel("official")
}
// Update initiates the self update process
func (u *Updater) Update() error {
path, err := os.Executable()
if err != nil {
return err
}
if resolvedPath, err := filepath.EvalSymlinks(path); err == nil {
path = resolvedPath
}
// go fetch latest updates manifest
err = u.fetchInfo()
if err != nil {
return err
}
// 检测是新版本才更新,否则不更新
cmp := semver.Compare(u.Info.Version, u.CurrentVersion)
if cmp <= 0 {
return nil
}
old, err := os.Open(path)
if err != nil {
return err
}
defer old.Close()
// if patch failed grab the full new bin
bin, err := u.fetchAndVerifyFullBin()
if err != nil {
if err == ErrHashMismatch {
log.Println("update: hash mismatch from full binary")
} else {
log.Println("update: fetching full binary,", err)
}
return err
}
// close the old binary before installing because on windows
// it can't be renamed if a handle to the file is still open
old.Close()
err, errRecover := fromStream(bytes.NewBuffer(bin))
if errRecover != nil {
return fmt.Errorf("update and recovery errors: %q %q", err, errRecover)
}
if err != nil {
return err
}
// update was successful, run func if set
if u.OnSuccessfulUpdate != nil {
u.OnSuccessfulUpdate()
}
return nil
}
// UpdateWithChannel 通过渠道来检测
func (u *Updater) UpdateWithChannel(channel string) error {
path, err := os.Executable()
if err != nil {
return err
}
if resolvedPath, err := filepath.EvalSymlinks(path); err == nil {
path = resolvedPath
}
if channel == "" || channel == "official" {
// go fetch latest updates manifest
err = u.fetchInfo()
if err != nil {
return err
}
// 检测是新版本才更新,否则不更新
cmp := semver.Compare(u.Info.Version, u.CurrentVersion)
if cmp <= 0 {
return nil
}
old, err := os.Open(path)
if err != nil {
return err
}
defer old.Close()
// if patch failed grab the full new bin
bin, err := u.fetchAndVerifyFullBin()
if err != nil {
if err == ErrHashMismatch {
log.Println("update: hash mismatch from full binary")
} else {
log.Println("update: fetching full binary,", err)
}
return err
}
// close the old binary before installing because on windows
// it can't be renamed if a handle to the file is still open
old.Close()
err, errRecover := fromStream(bytes.NewBuffer(bin))
if errRecover != nil {
return fmt.Errorf("update and recovery errors: %q %q", err, errRecover)
}
if err != nil {
return err
}
// update was successful, run func if set
if u.OnSuccessfulUpdate != nil {
u.OnSuccessfulUpdate()
}
} else if channel == "github" {
err = u.fetchInfoGithub()
if err != nil {
return err
}
// 从 GitHub 下载资源
r, err := u.fetch(u.BinGithubURL)
if err != nil {
return err
}
defer r.Close()
// 创建临时目录用于解压文件
tempDir, err := ioutil.TempDir("", "samwaf_beta_update"+u.Info.Version)
if err != nil {
return err
}
defer os.RemoveAll(tempDir)
// 保存下载的文件到临时文件
tempFile := filepath.Join(tempDir, "download")
out, err := os.Create(tempFile)
if err != nil {
return err
}
_, err = io.Copy(out, r)
out.Close()
if err != nil {
return err
}
// 根据文件类型和平台解压并获取正确的可执行文件
var binPath string
if strings.HasSuffix(u.BinGithubURL, ".exe") {
//使用win7内核
binPath = tempFile
} else if strings.HasSuffix(u.BinGithubURL, ".zip") {
// 情况 1: 从 ZIP 中提取 SamWaf64.exe
err = utils.Unzip(tempFile, tempDir)
if err != nil {
return err
}
binPath = filepath.Join(tempDir, "SamWaf64.exe")
} else if strings.HasSuffix(u.BinGithubURL, ".tar.gz") {
// 处理 Linux 平台的 tar.gz 文件
err = utils.ExtractTarGz(tempFile, tempDir)
if err != nil {
return err
}
if strings.Contains(u.BinGithubURL, "Linux_x86_64") {
// 情况 2: 从 tar.gz 中提取 SamWafLinux64
binPath = filepath.Join(tempDir, "SamWafLinux64")
} else if strings.Contains(u.BinGithubURL, "Linux_arm64") {
// 情况 3: 从 tar.gz 中提取 SamWafLinuxArm64
binPath = filepath.Join(tempDir, "SamWafLinuxArm64")
}
}
// 检查是否找到了可执行文件
if binPath == "" {
return errors.New("无法找到适合当前平台的可执行文件")
}
fileBytes, err := ioutil.ReadFile(binPath)
if err != nil {
return err
}
err, errRecover := fromStream(bytes.NewBuffer(fileBytes))
if errRecover != nil {
return fmt.Errorf("update and recovery errors: %q %q", err, errRecover)
}
if err != nil {
return err
}
// update was successful, run func if set
if u.OnSuccessfulUpdate != nil {
u.OnSuccessfulUpdate()
}
}
return nil
}
func fromStream(updateWith io.Reader) (err error, errRecover error) {
updatePath, err := os.Executable()
if err != nil {
return
}
var newBytes []byte
newBytes, err = ioutil.ReadAll(updateWith)
if err != nil {
return
}
// get the directory the executable exists in
updateDir := filepath.Dir(updatePath)
filename := filepath.Base(updatePath)
// Copy the contents of of newbinary to a the 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 {
return
}
defer fp.Close()
_, err = io.Copy(fp, bytes.NewReader(newBytes))
// if we don't call fp.Close(), windows won't let us move the new executable
// because the file will still be "in use"
fp.Close()
// this is where we'll move the executable to so that we can swap in the updated replacement
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 update, 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)
// move the existing executable to a new file in the same directory
err = os.Rename(updatePath, oldPath)
if err != nil {
return
}
// move the new exectuable in to become the new program
err = os.Rename(newPath, updatePath)
if err != nil {
// copy unsuccessful
errRecover = os.Rename(oldPath, updatePath)
} else {
// copy successful, remove the old binary
errRemove := os.Remove(oldPath)
// windows has trouble with removing old binaries, so hide it instead
if errRemove != nil {
_ = hideFile(oldPath)
}
}
return
}
// fetchInfo fetches the update JSON manifest at u.ApiURL/appname/platform.json?v=currentVersion
// and updates u.Info.
func (u *Updater) fetchInfo() error {
r, err := u.fetch(u.ApiURL + url.QueryEscape(u.CmdName) + "/" + url.QueryEscape(plat) + ".json?v=" + global.GWAF_RELEASE_VERSION + "&u=" + global.GWAF_USER_CODE)
if err != nil {
return err
}
defer r.Close()
err = json.NewDecoder(r).Decode(&u.Info)
if err != nil {
return err
}
if len(u.Info.Sha256) != sha256.Size {
return errors.New("bad cmd hash in info")
}
return nil
}
// fetchInfoGithub 从GitHub获取最新beta版本信息
func (u *Updater) fetchInfoGithub() error {
r, err := u.fetch(global.GUPDATE_GITHUB_VERSION_URL)
if err != nil {
return err
}
defer r.Close()
// 解析GitHub API返回的JSON数据
var githubRelease struct {
TagName string `json:"tag_name"`
Name string `json:"name"`
Body string `json:"body"`
Assets []struct {
Name string `json:"name"`
BrowserDownloadURL string `json:"browser_download_url"`
Size int64 `json:"size"`
ContentType string `json:"content_type"`
} `json:"assets"`
}
err = json.NewDecoder(r).Decode(&githubRelease)
if err != nil {
return err
}
//判断tagname 是否包含beta
if !strings.Contains(githubRelease.TagName, "beta") {
//return errors.New("not beta version")
}
// 过滤掉debug版本,防止用户意外升级到debug版本
if strings.Contains(githubRelease.TagName, "debug") || strings.Contains(strings.ToLower(githubRelease.Name), "debug") {
return errors.New("debug version detected, skipping update to prevent accidental upgrade")
}
// 查找适合当前平台的资源
var downloadURL string
// 根据平台选择合适的下载文件
platformSuffix := ""
utils.IsSupportedWindows7Version()
switch plat {
case "windows-amd64":
if utils.IsSupportedWindows7Version() {
platformSuffix = "SamWaf64ForWin7Win8Win2008"
} else {
platformSuffix = "Windows_x86_64"
}
case "linux-amd64":
platformSuffix = "Linux_x86_64"
case "linux-arm64":
platformSuffix = "Linux_arm64"
case "darwin-amd64":
platformSuffix = "Darwin_x86_64"
case "darwin-arm64":
platformSuffix = "Darwin_arm64"
}
// 查找匹配当前平台的资源,同时过滤掉debug版本
for _, asset := range githubRelease.Assets {
// 跳过包含DEBUG_SYMBOLS的文件
if strings.Contains(asset.Name, "DEBUG_SYMBOLS") || strings.Contains(strings.ToUpper(asset.Name), "DEBUG") {
continue
}
if strings.Contains(asset.Name, platformSuffix) {
downloadURL = asset.BrowserDownloadURL
break
}
}
// 如果没有找到适合的资源,返回错误
if downloadURL == "" {
return errors.New("no suitable release asset found for this platform")
}
u.BinGithubURL = downloadURL
u.Info.Version = githubRelease.TagName
u.Info.Desc = githubRelease.Body
return nil
}
func (u *Updater) fetchAndVerifyPatch(old io.Reader) ([]byte, error) {
bin, err := u.fetchAndApplyPatch(old)
if err != nil {
return nil, err
}
if !verifySha(bin, u.Info.Sha256) {
return nil, ErrHashMismatch
}
return bin, nil
}
func (u *Updater) fetchAndApplyPatch(old io.Reader) ([]byte, error) {
r, err := u.fetch(u.DiffURL + url.QueryEscape(u.CmdName) + "/" + url.QueryEscape(u.CurrentVersion) + "/" + url.QueryEscape(u.Info.Version) + "/" + url.QueryEscape(plat))
if err != nil {
return nil, err
}
defer r.Close()
var buf bytes.Buffer
err = binarydist.Patch(old, &buf, r)
return buf.Bytes(), err
}
func (u *Updater) fetchAndVerifyFullBin() ([]byte, error) {
bin, err := u.fetchBin()
if err != nil {
return nil, err
}
verified := verifySha(bin, u.Info.Sha256)
if !verified {
return nil, ErrHashMismatch
}
return bin, nil
}
func (u *Updater) fetchBin() ([]byte, error) {
r, err := u.fetch(u.BinURL + url.QueryEscape(u.CmdName) + "/" + url.QueryEscape(u.Info.Version) + "/" + url.QueryEscape(plat) + ".gz")
if err != nil {
return nil, err
}
defer r.Close()
buf := new(bytes.Buffer)
gz, err := gzip.NewReader(r)
if err != nil {
return nil, err
}
if _, err = io.Copy(buf, gz); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func (u *Updater) fetch(url string) (io.ReadCloser, error) {
if u.Requester == nil {
return defaultHTTPRequester.Fetch(url)
}
readCloser, err := u.Requester.Fetch(url)
if err != nil {
return nil, err
}
if readCloser == nil {
return nil, fmt.Errorf("Fetch was expected to return non-nil ReadCloser")
}
return readCloser, nil
}
func (u *Updater) GetHttps(url string) {
}
func readTime(path string) time.Time {
p, err := ioutil.ReadFile(path)
if os.IsNotExist(err) {
return time.Time{}
}
if err != nil {
return time.Now().Add(1000 * time.Hour)
}
t, err := time.Parse(time.RFC3339, string(p))
if err != nil {
return time.Now().Add(1000 * time.Hour)
}
return t
}
func verifySha(bin []byte, sha []byte) bool {
h := sha256.New()
h.Write(bin)
return bytes.Equal(h.Sum(nil), sha)
}
func writeTime(path string, t time.Time) bool {
return ioutil.WriteFile(path, []byte(t.Format(time.RFC3339)), 0644) == nil
}
// BackupExecutable 备份当前可执行文件,并写入版本号 sidecar 文件
func BackupExecutable() error {
execPath, err := os.Executable()
if err != nil {
return err
}
if resolvedPath, err := filepath.EvalSymlinks(execPath); err == nil {
execPath = resolvedPath
}
currentDir := utils.GetCurrentDir()
backupDir := filepath.Join(currentDir, "data", "backups_bin")
fileName := filepath.Base(execPath)
fileNameWithoutExt := strings.TrimSuffix(fileName, filepath.Ext(fileName))
backupPath, err := utils.BackupFile(execPath, backupDir, fileNameWithoutExt, 5)
if err != nil {
return err
}
// 写版本 sidecar,供 ListBackups 读取
sidecarPath := strings.TrimSuffix(backupPath, filepath.Ext(backupPath)) + ".version"
_ = os.WriteFile(sidecarPath, []byte(global.GWAF_RELEASE_VERSION), 0644)
return nil
}
// BackupInfo 描述一个备份版本的元信息
type BackupInfo struct {
FileName string `json:"file_name"`
Version string `json:"version"` // 来自 sidecar;旧备份无 sidecar 时为 "unknown"
BackupTime time.Time `json:"backup_time"` // 文件 ModTime
FileSize int64 `json:"file_size"`
}
// ListBackups 列出所有可用的备份版本,按时间倒序(最新在前)
func ListBackups() ([]BackupInfo, error) {
currentDir := utils.GetCurrentDir()
backupDir := filepath.Join(currentDir, "data", "backups_bin")
entries, err := os.ReadDir(backupDir)
if err != nil {
if os.IsNotExist(err) {
return []BackupInfo{}, nil
}
return nil, err
}
execPath, err := os.Executable()
if err != nil {
return nil, err
}
if resolvedPath, err := filepath.EvalSymlinks(execPath); err == nil {
execPath = resolvedPath
}
fileName := filepath.Base(execPath)
fileNameWithoutExt := strings.TrimSuffix(fileName, filepath.Ext(fileName))
var list []BackupInfo
for _, entry := range entries {
if entry.IsDir() {
continue
}
name := entry.Name()
// 跳过 sidecar 文件
if strings.HasSuffix(name, ".version") {
continue
}
// 只处理与当前程序同名前缀的备份
if !strings.HasPrefix(name, fileNameWithoutExt+"_") {
continue
}
info, err := entry.Info()
if err != nil {
continue
}
bi := BackupInfo{
FileName: name,
Version: "unknown",
BackupTime: info.ModTime(),
FileSize: info.Size(),
}
// 尝试读取版本 sidecar
sidecarPath := filepath.Join(backupDir, strings.TrimSuffix(name, filepath.Ext(name))+".version")
if versionBytes, err := os.ReadFile(sidecarPath); err == nil {
bi.Version = strings.TrimSpace(string(versionBytes))
}
list = append(list, bi)
}
sort.Slice(list, func(i, j int) bool {
return list[i].BackupTime.After(list[j].BackupTime)
})
return list, nil
}
// RollbackExecutable 将可执行文件回退到指定版本的备份
// version 为空时取最新备份
func RollbackExecutable(version string) error {
list, err := ListBackups()
if err != nil {
return fmt.Errorf("列出备份失败: %w", err)
}
if len(list) == 0 {
return fmt.Errorf("没有可用的备份版本")
}
var target *BackupInfo
if version == "" {
target = &list[0]
} else {
for i := range list {
if list[i].Version == version {
target = &list[i]
break
}
}
}
if target == nil {
return fmt.Errorf("未找到版本 %s 的备份", version)
}
execPath, err := os.Executable()
if err != nil {
return err
}
if resolvedPath, err := filepath.EvalSymlinks(execPath); err == nil {
execPath = resolvedPath
}
updateDir := filepath.Dir(execPath)
filename := filepath.Base(execPath)
currentDir := utils.GetCurrentDir()
backupDir := filepath.Join(currentDir, "data", "backups_bin")
backupFilePath := filepath.Join(backupDir, target.FileName)
// 将备份复制到临时文件
rollbackPath := filepath.Join(updateDir, fmt.Sprintf(".%s.rollback", filename))
src, err := os.Open(backupFilePath)
if err != nil {
return fmt.Errorf("打开备份文件失败: %w", err)
}
dst, err := os.OpenFile(rollbackPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0755)
if err != nil {
src.Close()
return fmt.Errorf("创建临时文件失败: %w", err)
}
_, copyErr := io.Copy(dst, src)
src.Close()
dst.Close()
if copyErr != nil {
return fmt.Errorf("复制备份文件失败: %w", copyErr)
}
// 与 fromStream() 相同的 rename 技巧(Windows 运行中的 exe 允许 rename 但不允许 overwrite
oldPath := filepath.Join(updateDir, fmt.Sprintf(".%s.old", filename))
_ = os.Remove(oldPath)
if err = os.Rename(execPath, oldPath); err != nil {
return fmt.Errorf("重命名当前程序失败: %w", err)
}
if err = os.Rename(rollbackPath, execPath); err != nil {
// 尝试恢复
_ = os.Rename(oldPath, execPath)
return fmt.Errorf("替换程序失败: %w", err)
}
return nil
}