upgradeDownload.go 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  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. "context"
  22. "fmt"
  23. "net/http"
  24. "os"
  25. "strconv"
  26. "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/errors"
  27. "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/parameters"
  28. utls "github.com/Psiphon-Labs/utls"
  29. )
  30. // DownloadUpgrade performs a resumable download of client upgrade files.
  31. //
  32. // While downloading/resuming, a temporary file is used. Once the download is complete,
  33. // a notice is issued and the upgrade is available at the destination specified in
  34. // config.GetUpgradeDownloadFilename().
  35. //
  36. // The upgrade download may be either tunneled or untunneled. As the untunneled case may
  37. // happen with no handshake request response, the downloader cannot rely on having the
  38. // upgrade_client_version output from handshake and instead this logic performs a
  39. // comparison between the config.ClientVersion and the client version recorded in the
  40. // remote entity's UpgradeDownloadClientVersionHeader. A HEAD request is made to check the
  41. // version before proceeding with a full download.
  42. //
  43. // NOTE: This code does not check that any existing file at config.GetUpgradeDownloadFilename()
  44. // is actually the version specified in handshakeVersion.
  45. //
  46. // TODO: This logic requires the outer client to *omit* config.UpgradeDownloadURLs, disabling
  47. // upgrade downloads, when there's already a downloaded upgrade pending. This is because the
  48. // outer client currently handles the authenticated package phase, and because the outer client
  49. // deletes the intermediate files (including config.GetUpgradeDownloadFilename()). So if the outer
  50. // client does not disable upgrade downloads then the new version will be downloaded
  51. // repeatedly. Implement a new scheme where tunnel core does the authenticated package phase
  52. // and tracks the the output by version number so that (a) tunnel core knows when it's not
  53. // necessary to re-download; (b) newer upgrades will be downloaded even when an older
  54. // upgrade is still pending install by the outer client.
  55. func DownloadUpgrade(
  56. ctx context.Context,
  57. config *Config,
  58. attempt int,
  59. handshakeVersion string,
  60. tunnel *Tunnel,
  61. untunneledDialConfig *DialConfig,
  62. tlsCache utls.ClientSessionCache) error {
  63. // Note: this downloader doesn't use ETags since many client binaries, with
  64. // different embedded values, exist for a single version.
  65. // Check if complete file already downloaded
  66. if _, err := os.Stat(config.GetUpgradeDownloadFilename()); err == nil {
  67. NoticeClientUpgradeDownloaded(config.GetUpgradeDownloadFilename())
  68. return nil
  69. }
  70. p := config.GetParameters().Get()
  71. urls := p.TransferURLs(parameters.UpgradeDownloadURLs)
  72. clientVersionHeader := p.String(parameters.UpgradeDownloadClientVersionHeader)
  73. downloadTimeout := p.Duration(parameters.FetchUpgradeTimeout)
  74. p.Close()
  75. var cancelFunc context.CancelFunc
  76. ctx, cancelFunc = context.WithTimeout(ctx, downloadTimeout)
  77. defer cancelFunc()
  78. // Select tunneled or untunneled configuration
  79. downloadURL := urls.Select(attempt)
  80. payloadSecure := true
  81. frontingUseDeviceBinder := true
  82. httpClient, _, _, err := MakeDownloadHTTPClient(
  83. ctx,
  84. config,
  85. tunnel,
  86. untunneledDialConfig,
  87. tlsCache,
  88. downloadURL.SkipVerify,
  89. config.DisableSystemRootCAs,
  90. payloadSecure,
  91. downloadURL.FrontingSpecs,
  92. frontingUseDeviceBinder,
  93. func(frontingProviderID string) {
  94. NoticeInfo(
  95. "DownloadUpgrade: selected fronting provider %s for %s",
  96. frontingProviderID, downloadURL.URL)
  97. })
  98. if err != nil {
  99. return errors.Trace(err)
  100. }
  101. // If no handshake version is supplied, make an initial HEAD request
  102. // to get the current version from the version header.
  103. availableClientVersion := handshakeVersion
  104. if availableClientVersion == "" {
  105. request, err := http.NewRequest("HEAD", downloadURL.URL, nil)
  106. if err != nil {
  107. return errors.Trace(err)
  108. }
  109. request = request.WithContext(ctx)
  110. response, err := httpClient.Do(request)
  111. if err == nil && response.StatusCode != http.StatusOK {
  112. response.Body.Close()
  113. err = fmt.Errorf("unexpected response status code: %d", response.StatusCode)
  114. }
  115. if err != nil {
  116. return errors.Trace(err)
  117. }
  118. defer response.Body.Close()
  119. currentClientVersion, err := strconv.Atoi(config.ClientVersion)
  120. if err != nil {
  121. return errors.Trace(err)
  122. }
  123. // Note: if the header is missing, Header.Get returns "" and then
  124. // strconv.Atoi returns a parse error.
  125. availableClientVersion = response.Header.Get(clientVersionHeader)
  126. checkAvailableClientVersion, err := strconv.Atoi(availableClientVersion)
  127. if err != nil {
  128. // If the header is missing or malformed, we can't determine the available
  129. // version number. This is unexpected; but if it happens, it's likely due
  130. // to a server-side configuration issue. In this one case, we don't
  131. // return an error so that we don't go into a rapid retry loop making
  132. // ineffective HEAD requests (the client may still signal an upgrade
  133. // download later in the session).
  134. NoticeWarning(
  135. "failed to download upgrade: invalid %s header value %s: %s",
  136. clientVersionHeader, availableClientVersion, err)
  137. return nil
  138. }
  139. if currentClientVersion >= checkAvailableClientVersion {
  140. NoticeClientIsLatestVersion(availableClientVersion)
  141. return nil
  142. }
  143. }
  144. // Proceed with download
  145. // An intermediate filename is used since the presence of
  146. // config.GetUpgradeDownloadFilename() indicates a completed download.
  147. downloadFilename := fmt.Sprintf(
  148. "%s.%s", config.GetUpgradeDownloadFilename(), availableClientVersion)
  149. n, _, err := ResumeDownload(
  150. ctx,
  151. httpClient,
  152. downloadURL.URL,
  153. MakePsiphonUserAgent(config),
  154. downloadFilename,
  155. "")
  156. NoticeClientUpgradeDownloadedBytes(n)
  157. if err != nil {
  158. return errors.Trace(err)
  159. }
  160. err = os.Rename(downloadFilename, config.GetUpgradeDownloadFilename())
  161. if err != nil {
  162. return errors.Trace(err)
  163. }
  164. NoticeClientUpgradeDownloaded(config.GetUpgradeDownloadFilename())
  165. // Limitation: unlike the remote server list download case, DNS cache
  166. // extension is not invoked here since payload authentication is not
  167. // currently implemented at this level. iOS VPN, the primary use case for
  168. // DNS cache extension, does not use this side-load upgrade mechanism.
  169. return nil
  170. }