2018-10-08 19:20:28 +00:00
package updater
2018-05-01 23:45:06 +00:00
import (
2019-06-18 16:47:29 +00:00
"context"
2020-04-28 00:55:27 +00:00
"fmt"
2018-05-01 23:45:06 +00:00
"os"
"runtime"
"time"
"golang.org/x/crypto/ssh/terminal"
"gopkg.in/urfave/cli.v2"
2020-04-29 20:51:32 +00:00
"github.com/cloudflare/cloudflared/logger"
2018-05-01 23:45:06 +00:00
"github.com/equinox-io/equinox"
"github.com/facebookgo/grace/gracenet"
2020-04-29 20:51:32 +00:00
"github.com/pkg/errors"
2018-05-01 23:45:06 +00:00
)
const (
2019-06-18 16:47:29 +00:00
DefaultCheckUpdateFreq = time . Hour * 24
2018-05-01 23:45:06 +00:00
appID = "app_idCzgxYerVD"
noUpdateInShellMessage = "cloudflared will not automatically update when run from the shell. To enable auto-updates, run cloudflared as a service: https://developers.cloudflare.com/argo-tunnel/reference/service/"
noUpdateOnWindowsMessage = "cloudflared will not automatically update on Windows systems."
)
2018-10-08 19:20:28 +00:00
var (
publicKey = [ ] byte ( `
2018-05-01 23:45:06 +00:00
-- -- - BEGIN ECDSA PUBLIC KEY -- -- -
MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE4OWZocTVZ8Do / L6ScLdkV + 9 A0IYMHoOf
dsCmJ / QZ6aw0w9qkkwEpne1Lmo6 + 0 pGexZzFZOH6w5amShn + RXt7qkSid9iWlzGq
EKx0BZogHSor9Wy5VztdFaAaVbsJiCbO
-- -- - END ECDSA PUBLIC KEY -- -- -
` )
2018-10-08 19:20:28 +00:00
)
2018-05-01 23:45:06 +00:00
2020-04-28 00:55:27 +00:00
// BinaryUpdated implements ExitCoder interface, the app will exit with status code 11
// https://pkg.go.dev/gopkg.in/urfave/cli.v2?tab=doc#ExitCoder
type statusSuccess struct {
newVersion string
}
func ( u * statusSuccess ) Error ( ) string {
return fmt . Sprintf ( "cloudflared has been updated to version %s" , u . newVersion )
}
func ( u * statusSuccess ) ExitCode ( ) int {
return 11
}
// UpdateErr implements ExitCoder interface, the app will exit with status code 10
type statusErr struct {
err error
}
func ( e * statusErr ) Error ( ) string {
return fmt . Sprintf ( "failed to update cloudflared: %v" , e . err )
}
func ( e * statusErr ) ExitCode ( ) int {
return 10
}
2019-02-14 22:26:33 +00:00
type UpdateOutcome struct {
2018-05-01 23:45:06 +00:00
Updated bool
Version string
Error error
}
2019-02-14 22:26:33 +00:00
func ( uo * UpdateOutcome ) noUpdate ( ) bool {
return uo . Error != nil && uo . Updated == false
}
func checkForUpdateAndApply ( ) UpdateOutcome {
2018-05-01 23:45:06 +00:00
var opts equinox . Options
if err := opts . SetPublicKeyPEM ( publicKey ) ; err != nil {
2019-02-14 22:26:33 +00:00
return UpdateOutcome { Error : err }
2018-05-01 23:45:06 +00:00
}
resp , err := equinox . Check ( appID , opts )
switch {
case err == equinox . NotAvailableErr :
2019-02-14 22:26:33 +00:00
return UpdateOutcome { }
2018-05-01 23:45:06 +00:00
case err != nil :
2019-02-14 22:26:33 +00:00
return UpdateOutcome { Error : err }
2018-05-01 23:45:06 +00:00
}
err = resp . Apply ( )
if err != nil {
2019-02-14 22:26:33 +00:00
return UpdateOutcome { Error : err }
2018-05-01 23:45:06 +00:00
}
2019-02-14 22:26:33 +00:00
return UpdateOutcome { Updated : true , Version : resp . ReleaseVersion }
2018-05-01 23:45:06 +00:00
}
2018-10-08 19:20:28 +00:00
func Update ( _ * cli . Context ) error {
2020-04-29 20:51:32 +00:00
logger , err := logger . New ( )
if err != nil {
return errors . Wrap ( err , "error setting up logger" )
}
updateOutcome := loggedUpdate ( logger )
2019-02-14 22:26:33 +00:00
if updateOutcome . Error != nil {
2020-04-28 00:55:27 +00:00
return & statusErr { updateOutcome . Error }
2019-02-14 22:26:33 +00:00
}
if updateOutcome . noUpdate ( ) {
logger . Infof ( "cloudflared is up to date (%s)" , updateOutcome . Version )
2020-04-28 00:55:27 +00:00
return nil
2018-05-01 23:45:06 +00:00
}
2019-02-14 22:26:33 +00:00
2020-04-28 00:55:27 +00:00
return & statusSuccess { newVersion : updateOutcome . Version }
2018-05-01 23:45:06 +00:00
}
2019-02-14 22:26:33 +00:00
// Checks for an update and applies it if one is available
2020-04-29 20:51:32 +00:00
func loggedUpdate ( logger logger . Service ) UpdateOutcome {
2019-02-14 22:26:33 +00:00
updateOutcome := checkForUpdateAndApply ( )
if updateOutcome . Updated {
logger . Infof ( "cloudflared has been updated to version %s" , updateOutcome . Version )
2018-05-01 23:45:06 +00:00
}
2019-02-14 22:26:33 +00:00
if updateOutcome . Error != nil {
2020-04-29 20:51:32 +00:00
logger . Errorf ( "update check failed: %s" , updateOutcome . Error )
2018-05-01 23:45:06 +00:00
}
2019-02-14 22:26:33 +00:00
return updateOutcome
2018-05-01 23:45:06 +00:00
}
2019-06-18 16:47:29 +00:00
// AutoUpdater periodically checks for new version of cloudflared.
type AutoUpdater struct {
configurable * configurable
listeners * gracenet . Net
updateConfigChan chan * configurable
2020-04-29 20:51:32 +00:00
logger logger . Service
2019-06-18 16:47:29 +00:00
}
// AutoUpdaterConfigurable is the attributes of AutoUpdater that can be reconfigured during runtime
type configurable struct {
enabled bool
freq time . Duration
}
2020-04-29 20:51:32 +00:00
func NewAutoUpdater ( freq time . Duration , listeners * gracenet . Net , logger logger . Service ) * AutoUpdater {
2019-06-18 16:47:29 +00:00
updaterConfigurable := & configurable {
enabled : true ,
freq : freq ,
}
if freq == 0 {
updaterConfigurable . enabled = false
updaterConfigurable . freq = DefaultCheckUpdateFreq
}
return & AutoUpdater {
configurable : updaterConfigurable ,
listeners : listeners ,
updateConfigChan : make ( chan * configurable ) ,
2020-04-29 20:51:32 +00:00
logger : logger ,
2019-06-18 16:47:29 +00:00
}
}
func ( a * AutoUpdater ) Run ( ctx context . Context ) error {
ticker := time . NewTicker ( a . configurable . freq )
for {
if a . configurable . enabled {
2020-04-29 20:51:32 +00:00
updateOutcome := loggedUpdate ( a . logger )
2019-06-18 16:47:29 +00:00
if updateOutcome . Updated {
os . Args = append ( os . Args , "--is-autoupdated=true" )
2020-04-28 00:55:27 +00:00
if IsSysV ( ) {
// SysV doesn't have a mechanism to keep service alive, we have to restart the process
2020-04-29 20:51:32 +00:00
a . logger . Info ( "Restarting service managed by SysV..." )
2020-04-28 00:55:27 +00:00
pid , err := a . listeners . StartProcess ( )
if err != nil {
2020-04-29 20:51:32 +00:00
a . logger . Errorf ( "Unable to restart server automatically: %s" , err )
2020-04-28 00:55:27 +00:00
return & statusErr { err : err }
}
// stop old process after autoupdate. Otherwise we create a new process
// after each update
2020-04-29 20:51:32 +00:00
a . logger . Infof ( "PID of the new process is %d" , pid )
2019-06-18 16:47:29 +00:00
}
2020-04-28 00:55:27 +00:00
return & statusSuccess { newVersion : updateOutcome . Version }
2019-06-18 16:47:29 +00:00
}
}
select {
case <- ctx . Done ( ) :
return ctx . Err ( )
case newConfigurable := <- a . updateConfigChan :
ticker . Stop ( )
a . configurable = newConfigurable
ticker = time . NewTicker ( a . configurable . freq )
// Check if there is new version of cloudflared after receiving new AutoUpdaterConfigurable
case <- ticker . C :
}
}
}
// Update is the method to pass new AutoUpdaterConfigurable to a running AutoUpdater. It is safe to be called concurrently
func ( a * AutoUpdater ) Update ( newFreq time . Duration ) {
newConfigurable := & configurable {
enabled : true ,
freq : newFreq ,
}
// A ero duration means autoupdate is disabled
if newFreq == 0 {
newConfigurable . enabled = false
newConfigurable . freq = DefaultCheckUpdateFreq
}
a . updateConfigChan <- newConfigurable
}
2020-04-29 20:51:32 +00:00
func IsAutoupdateEnabled ( c * cli . Context , l logger . Service ) bool {
if ! SupportAutoUpdate ( l ) {
2019-06-18 16:47:29 +00:00
return false
}
return ! c . Bool ( "no-autoupdate" ) && c . Duration ( "autoupdate-freq" ) != 0
}
2020-04-29 20:51:32 +00:00
func SupportAutoUpdate ( logger logger . Service ) bool {
2018-05-01 23:45:06 +00:00
if runtime . GOOS == "windows" {
logger . Info ( noUpdateOnWindowsMessage )
return false
}
if isRunningFromTerminal ( ) {
logger . Info ( noUpdateInShellMessage )
return false
}
2019-06-18 16:47:29 +00:00
return true
2018-05-01 23:45:06 +00:00
}
func isRunningFromTerminal ( ) bool {
return terminal . IsTerminal ( int ( os . Stdout . Fd ( ) ) )
}
2020-04-28 00:55:27 +00:00
func IsSysV ( ) bool {
if runtime . GOOS != "linux" {
return false
}
if _ , err := os . Stat ( "/run/systemd/system" ) ; err == nil {
return false
}
return true
}