mirror of
https://github.com/fumiama/terasu-cloudflared.git
synced 2026-06-07 01:50:23 +08:00
In the rare case that the updater downloads the same binary (validated via checksum) we want to make sure that the updater does not attempt to upgrade and restart the cloudflared process. The binaries are equivalent and this would provide no value. However, we are covering this case because there was an errant deployment of cloudflared that reported itself as an older version and was then stuck in an infinite loop attempting to upgrade to the latest version which didn't exist. By making sure that the binary is different ensures that the upgrade will be attempted and cloudflared will be restarted to run the new version. This change only affects cloudflared tunnels running with default settings or `--no-autoupdate=false` which allows cloudflared to auto-update itself in-place. Most distributions that handle package management at the operating system level are not affected by this change.
256 lines
7.1 KiB
Go
256 lines
7.1 KiB
Go
package updater
|
|
|
|
import (
|
|
"archive/tar"
|
|
"compress/gzip"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"text/template"
|
|
"time"
|
|
|
|
"github.com/getsentry/sentry-go"
|
|
|
|
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
|
)
|
|
|
|
const (
|
|
clientTimeout = time.Second * 60
|
|
// stop the service
|
|
// rename cloudflared.exe to cloudflared.exe.old
|
|
// rename cloudflared.exe.new to cloudflared.exe
|
|
// delete cloudflared.exe.old
|
|
// start the service
|
|
// exit with code 0 if we've reached this point indicating success.
|
|
windowsUpdateCommandTemplate = `sc stop cloudflared >nul 2>&1
|
|
rename "{{.TargetPath}}" {{.OldName}}
|
|
rename "{{.NewPath}}" {{.BinaryName}}
|
|
del "{{.OldPath}}"
|
|
sc start cloudflared >nul 2>&1
|
|
exit /b 0`
|
|
batchFileName = "cfd_update.bat"
|
|
)
|
|
|
|
// Prepare some data to insert into the template.
|
|
type batchData struct {
|
|
TargetPath string
|
|
OldName string
|
|
NewPath string
|
|
OldPath string
|
|
BinaryName string
|
|
BatchName string
|
|
}
|
|
|
|
// WorkersVersion implements the Version interface.
|
|
// It contains everything needed to perform a version upgrade
|
|
type WorkersVersion struct {
|
|
downloadURL string
|
|
checksum string
|
|
version string
|
|
targetPath string
|
|
isCompressed bool
|
|
userMessage string
|
|
}
|
|
|
|
// NewWorkersVersion creates a new Version object. This is normally created by a WorkersService JSON checkin response
|
|
// url is where to download the file
|
|
// version is the version of this update
|
|
// checksum is the expected checksum of the downloaded file
|
|
// target path is where the file should be replace. Normally this the running cloudflared's path
|
|
// userMessage is a possible message to convey back to the user after having checked in with the Updater Service
|
|
// isCompressed tells whether the asset to update cloudflared is compressed or not
|
|
func NewWorkersVersion(url, version, checksum, targetPath, userMessage string, isCompressed bool) CheckResult {
|
|
return &WorkersVersion{
|
|
downloadURL: url,
|
|
version: version,
|
|
checksum: checksum,
|
|
targetPath: targetPath,
|
|
isCompressed: isCompressed,
|
|
userMessage: userMessage,
|
|
}
|
|
}
|
|
|
|
// Apply does the actual verification and update logic.
|
|
// This includes signature and checksum validation,
|
|
// replacing the binary, etc
|
|
func (v *WorkersVersion) Apply() error {
|
|
newFilePath := fmt.Sprintf("%s.new", v.targetPath)
|
|
os.Remove(newFilePath) //remove any failed updates before download
|
|
|
|
// download the file
|
|
if err := download(v.downloadURL, newFilePath, v.isCompressed); err != nil {
|
|
return err
|
|
}
|
|
|
|
downloadSum, err := cliutil.FileChecksum(newFilePath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Check that the file downloaded matches what is expected.
|
|
if v.checksum != downloadSum {
|
|
return errors.New("checksum validation failed")
|
|
}
|
|
|
|
// Check if the currently running version has the same checksum
|
|
if downloadSum == buildInfo.Checksum {
|
|
// Currently running binary matches the downloaded binary so we have no reason to update. This is
|
|
// typically unexpected, as such we emit a sentry event.
|
|
localHub := sentry.CurrentHub().Clone()
|
|
err := errors.New("checksum validation matches currently running process")
|
|
localHub.CaptureException(err)
|
|
// Make sure to cleanup the new downloaded file since we aren't upgrading versions.
|
|
os.Remove(newFilePath)
|
|
return err
|
|
}
|
|
|
|
oldFilePath := fmt.Sprintf("%s.old", v.targetPath)
|
|
// Windows requires more effort to self update, especially when it is running as a service:
|
|
// you have to stop the service (if running as one) in order to move/rename the binary
|
|
// but now the binary isn't running though, so an external process
|
|
// has to move the old binary out and the new one in then start the service
|
|
// the easiest way to do this is with a batch file (or with a DLL, but that gets ugly for a cross compiled binary like cloudflared)
|
|
// a batch file isn't ideal, but it is the simplest path forward for the constraints Windows creates
|
|
if runtime.GOOS == "windows" {
|
|
if err := writeBatchFile(v.targetPath, newFilePath, oldFilePath); err != nil {
|
|
return err
|
|
}
|
|
rootDir := filepath.Dir(v.targetPath)
|
|
batchPath := filepath.Join(rootDir, batchFileName)
|
|
return runWindowsBatch(batchPath)
|
|
}
|
|
|
|
// now move the current file out, move the new file in and delete the old file
|
|
if err := os.Rename(v.targetPath, oldFilePath); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := os.Rename(newFilePath, v.targetPath); err != nil {
|
|
//attempt rollback
|
|
os.Rename(oldFilePath, v.targetPath)
|
|
return err
|
|
}
|
|
os.Remove(oldFilePath)
|
|
|
|
return nil
|
|
}
|
|
|
|
// String returns the version number of this update/release (e.g. 2020.08.05)
|
|
func (v *WorkersVersion) Version() string {
|
|
return v.version
|
|
}
|
|
|
|
// String returns a possible message to convey back to user after having checked in with the Updater Service. E.g.
|
|
// it can warn about the need to update the version currently running.
|
|
func (v *WorkersVersion) UserMessage() string {
|
|
return v.userMessage
|
|
}
|
|
|
|
// download the file from the link in the json
|
|
func download(url, filepath string, isCompressed bool) error {
|
|
client := &http.Client{
|
|
Timeout: clientTimeout,
|
|
}
|
|
resp, err := client.Get(url)
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
var r io.Reader
|
|
r = resp.Body
|
|
|
|
// compressed macos binary, need to decompress
|
|
if isCompressed || isCompressedFile(url) {
|
|
// first the gzip reader
|
|
gr, err := gzip.NewReader(resp.Body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer gr.Close()
|
|
|
|
// now the tar
|
|
tr := tar.NewReader(gr)
|
|
|
|
// advance the reader pass the header, which will be the single binary file
|
|
tr.Next()
|
|
|
|
r = tr
|
|
}
|
|
|
|
out, err := os.OpenFile(filepath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0755)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer out.Close()
|
|
|
|
_, err = io.Copy(out, r)
|
|
return err
|
|
}
|
|
|
|
// isCompressedFile is a really simple file extension check to see if this is a macos tar and gzipped
|
|
func isCompressedFile(urlstring string) bool {
|
|
if strings.HasSuffix(urlstring, ".tgz") {
|
|
return true
|
|
}
|
|
|
|
u, err := url.Parse(urlstring)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return strings.HasSuffix(u.Path, ".tgz")
|
|
}
|
|
|
|
// writeBatchFile writes a batch file out to disk
|
|
// see the dicussion on why it has to be done this way
|
|
func writeBatchFile(targetPath string, newPath string, oldPath string) error {
|
|
batchFilePath := filepath.Join(filepath.Dir(targetPath), batchFileName)
|
|
os.Remove(batchFilePath) //remove any failed updates before download
|
|
f, err := os.Create(batchFilePath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
cfdName := filepath.Base(targetPath)
|
|
oldName := filepath.Base(oldPath)
|
|
|
|
data := batchData{
|
|
TargetPath: targetPath,
|
|
OldName: oldName,
|
|
NewPath: newPath,
|
|
OldPath: oldPath,
|
|
BinaryName: cfdName,
|
|
BatchName: batchFileName,
|
|
}
|
|
|
|
t, err := template.New("batch").Parse(windowsUpdateCommandTemplate)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return t.Execute(f, data)
|
|
}
|
|
|
|
// run each OS command for windows
|
|
func runWindowsBatch(batchFile string) error {
|
|
defer os.Remove(batchFile)
|
|
cmd := exec.Command("cmd", "/C", batchFile)
|
|
_, err := cmd.Output()
|
|
// Remove the batch file we created. Don't let this interfere with the error
|
|
// we report.
|
|
if err != nil {
|
|
if exitError, ok := err.(*exec.ExitError); ok {
|
|
return fmt.Errorf("Error during update : %s;", string(exitError.Stderr))
|
|
}
|
|
|
|
}
|
|
return err
|
|
}
|