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  }