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