github.com/psiphon-Labs/psiphon-tunnel-core@v2.0.28+incompatible/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/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/errors" 30 "github.com/Psiphon-Labs/psiphon-tunnel-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 if err != nil { 97 return errors.Trace(err) 98 } 99 100 // If no handshake version is supplied, make an initial HEAD request 101 // to get the current version from the version header. 102 103 availableClientVersion := handshakeVersion 104 if availableClientVersion == "" { 105 106 request, err := http.NewRequest("HEAD", downloadURL.URL, nil) 107 if err != nil { 108 return errors.Trace(err) 109 } 110 111 request = request.WithContext(ctx) 112 113 response, err := httpClient.Do(request) 114 115 if err == nil && response.StatusCode != http.StatusOK { 116 response.Body.Close() 117 err = fmt.Errorf("unexpected response status code: %d", response.StatusCode) 118 } 119 if err != nil { 120 return errors.Trace(err) 121 } 122 defer response.Body.Close() 123 124 currentClientVersion, err := strconv.Atoi(config.ClientVersion) 125 if err != nil { 126 return errors.Trace(err) 127 } 128 129 // Note: if the header is missing, Header.Get returns "" and then 130 // strconv.Atoi returns a parse error. 131 availableClientVersion = response.Header.Get(clientVersionHeader) 132 checkAvailableClientVersion, err := strconv.Atoi(availableClientVersion) 133 if err != nil { 134 // If the header is missing or malformed, we can't determine the available 135 // version number. This is unexpected; but if it happens, it's likely due 136 // to a server-side configuration issue. In this one case, we don't 137 // return an error so that we don't go into a rapid retry loop making 138 // ineffective HEAD requests (the client may still signal an upgrade 139 // download later in the session). 140 NoticeWarning( 141 "failed to download upgrade: invalid %s header value %s: %s", 142 clientVersionHeader, availableClientVersion, err) 143 return nil 144 } 145 146 if currentClientVersion >= checkAvailableClientVersion { 147 NoticeClientIsLatestVersion(availableClientVersion) 148 return nil 149 } 150 } 151 152 // Proceed with download 153 154 // An intermediate filename is used since the presence of 155 // config.GetUpgradeDownloadFilename() indicates a completed download. 156 157 downloadFilename := fmt.Sprintf( 158 "%s.%s", config.GetUpgradeDownloadFilename(), availableClientVersion) 159 160 n, _, err := ResumeDownload( 161 ctx, 162 httpClient, 163 downloadURL.URL, 164 MakePsiphonUserAgent(config), 165 downloadFilename, 166 "") 167 168 NoticeClientUpgradeDownloadedBytes(n) 169 170 if err != nil { 171 return errors.Trace(err) 172 } 173 174 err = os.Rename(downloadFilename, config.GetUpgradeDownloadFilename()) 175 if err != nil { 176 return errors.Trace(err) 177 } 178 179 NoticeClientUpgradeDownloaded(config.GetUpgradeDownloadFilename()) 180 181 // Limitation: unlike the remote server list download case, DNS cache 182 // extension is not invoked here since payload authentication is not 183 // currently implemented at this level. iOS VPN, the primary use case for 184 // DNS cache extension, does not use this side-load upgrade mechanism. 185 186 return nil 187 }