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  }