upgradeDownload.go 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. /*
  2. * Copyright (c) 2015, Psiphon Inc.
  3. * All rights reserved.
  4. *
  5. * This program is free software: you can redistribute it and/or modify
  6. * it under the terms of the GNU General Public License as published by
  7. * the Free Software Foundation, either version 3 of the License, or
  8. * (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU General Public License
  16. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  17. *
  18. */
  19. package psiphon
  20. import (
  21. "fmt"
  22. "net/http"
  23. "os"
  24. "strconv"
  25. "time"
  26. "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
  27. )
  28. // DownloadUpgrade performs a resumable download of client upgrade files.
  29. //
  30. // While downloading/resuming, a temporary file is used. Once the download is complete,
  31. // a notice is issued and the upgrade is available at the destination specified in
  32. // config.UpgradeDownloadFilename.
  33. //
  34. // The upgrade download may be either tunneled or untunneled. As the untunneled case may
  35. // happen with no handshake request response, the downloader cannot rely on having the
  36. // upgrade_client_version output from handshake and instead this logic performs a
  37. // comparison between the config.ClientVersion and the client version recorded in the
  38. // remote entity's UpgradeDownloadClientVersionHeader. A HEAD request is made to check the
  39. // version before proceeding with a full download.
  40. //
  41. // NOTE: This code does not check that any existing file at config.UpgradeDownloadFilename
  42. // is actually the version specified in handshakeVersion.
  43. //
  44. // TODO: This logic requires the outer client to *omit* config.UpgradeDownloadFilename
  45. // when there's already a downloaded upgrade pending. Because the outer client currently
  46. // handles the authenticated package phase, and because the outer client deletes the
  47. // intermediate files (including config.UpgradeDownloadFilename), if the outer client
  48. // does not omit config.UpgradeDownloadFilename then the new version will be downloaded
  49. // repeatedly. Implement a new scheme where tunnel core does the authenticated package phase
  50. // and tracks the the output by version number so that (a) tunnel core knows when it's not
  51. // necessary to re-download; (b) newer upgrades will be downloaded even when an older
  52. // upgrade is still pending install by the outer client.
  53. func DownloadUpgrade(
  54. config *Config,
  55. attempt int,
  56. handshakeVersion string,
  57. tunnel *Tunnel,
  58. untunneledDialConfig *DialConfig) error {
  59. // Note: this downloader doesn't use ETags since many client binaries, with
  60. // different embedded values, exist for a single version.
  61. // Check if complete file already downloaded
  62. if _, err := os.Stat(config.UpgradeDownloadFilename); err == nil {
  63. NoticeClientUpgradeDownloaded(config.UpgradeDownloadFilename)
  64. return nil
  65. }
  66. // Select tunneled or untunneled configuration
  67. downloadURL, _, skipVerify := selectDownloadURL(attempt, config.UpgradeDownloadURLs)
  68. httpClient, requestUrl, err := MakeDownloadHttpClient(
  69. config,
  70. tunnel,
  71. untunneledDialConfig,
  72. downloadURL,
  73. skipVerify,
  74. time.Duration(*config.DownloadUpgradeTimeoutSeconds)*time.Second)
  75. // If no handshake version is supplied, make an initial HEAD request
  76. // to get the current version from the version header.
  77. availableClientVersion := handshakeVersion
  78. if availableClientVersion == "" {
  79. request, err := http.NewRequest("HEAD", requestUrl, nil)
  80. if err != nil {
  81. return common.ContextError(err)
  82. }
  83. response, err := httpClient.Do(request)
  84. if err == nil && response.StatusCode != http.StatusOK {
  85. response.Body.Close()
  86. err = fmt.Errorf("unexpected response status code: %d", response.StatusCode)
  87. }
  88. if err != nil {
  89. return common.ContextError(err)
  90. }
  91. defer response.Body.Close()
  92. currentClientVersion, err := strconv.Atoi(config.ClientVersion)
  93. if err != nil {
  94. return common.ContextError(err)
  95. }
  96. // Note: if the header is missing, Header.Get returns "" and then
  97. // strconv.Atoi returns a parse error.
  98. availableClientVersion = response.Header.Get(config.UpgradeDownloadClientVersionHeader)
  99. checkAvailableClientVersion, err := strconv.Atoi(availableClientVersion)
  100. if err != nil {
  101. // If the header is missing or malformed, we can't determine the available
  102. // version number. This is unexpected; but if it happens, it's likely due
  103. // to a server-side configuration issue. In this one case, we don't
  104. // return an error so that we don't go into a rapid retry loop making
  105. // ineffective HEAD requests (the client may still signal an upgrade
  106. // download later in the session).
  107. NoticeAlert(
  108. "failed to download upgrade: invalid %s header value %s: %s",
  109. config.UpgradeDownloadClientVersionHeader, availableClientVersion, err)
  110. return nil
  111. }
  112. if currentClientVersion >= checkAvailableClientVersion {
  113. NoticeClientIsLatestVersion(availableClientVersion)
  114. return nil
  115. }
  116. }
  117. // Proceed with download
  118. // An intermediate filename is used since the presence of
  119. // config.UpgradeDownloadFilename indicates a completed download.
  120. downloadFilename := fmt.Sprintf(
  121. "%s.%s", config.UpgradeDownloadFilename, availableClientVersion)
  122. n, _, err := ResumeDownload(
  123. httpClient,
  124. requestUrl,
  125. MakePsiphonUserAgent(config),
  126. downloadFilename,
  127. "")
  128. NoticeClientUpgradeDownloadedBytes(n)
  129. if err != nil {
  130. return common.ContextError(err)
  131. }
  132. err = os.Rename(downloadFilename, config.UpgradeDownloadFilename)
  133. if err != nil {
  134. return common.ContextError(err)
  135. }
  136. NoticeClientUpgradeDownloaded(config.UpgradeDownloadFilename)
  137. return nil
  138. }