github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/updater/util/http.go (about)

     1  // Copyright 2015 Keybase, Inc. All rights reserved. Use of
     2  // this source code is governed by the included BSD license.
     3  
     4  package util
     5  
     6  import (
     7  	"fmt"
     8  	"io"
     9  	"net/http"
    10  	"net/url"
    11  	"os"
    12  	"time"
    13  )
    14  
    15  const fileScheme = "file"
    16  
    17  func discardAndClose(rc io.ReadCloser) error {
    18  	_, _ = io.Copy(io.Discard, rc)
    19  	return rc.Close()
    20  }
    21  
    22  // DiscardAndCloseBody reads as much as possible from the body of the
    23  // given response, and then closes it.
    24  //
    25  // This is because, in order to free up the current connection for
    26  // re-use, a response body must be read from before being closed; see
    27  // http://stackoverflow.com/a/17953506 .
    28  //
    29  // Instead of doing:
    30  //
    31  //	res, _ := ...
    32  //	defer res.Body.Close()
    33  //
    34  // do
    35  //
    36  //	res, _ := ...
    37  //	defer DiscardAndCloseBody(res)
    38  //
    39  // instead.
    40  func DiscardAndCloseBody(resp *http.Response) error {
    41  	if resp == nil {
    42  		return fmt.Errorf("Nothing to discard (http.Response was nil)")
    43  	}
    44  	return discardAndClose(resp.Body)
    45  }
    46  
    47  // SaveHTTPResponse saves an http.Response to path
    48  func SaveHTTPResponse(resp *http.Response, savePath string, mode os.FileMode, log Log) error {
    49  	if resp == nil {
    50  		return fmt.Errorf("No response")
    51  	}
    52  	file, err := os.OpenFile(savePath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, mode)
    53  	if err != nil {
    54  		return err
    55  	}
    56  	defer Close(file)
    57  
    58  	log.Infof("Downloading to %s", savePath)
    59  	n, err := io.Copy(file, resp.Body)
    60  	if err == nil {
    61  		log.Infof("Downloaded %d bytes", n)
    62  	}
    63  	return err
    64  }
    65  
    66  // DiscardAndCloseBodyIgnoreError calls DiscardAndCloseBody.
    67  // This satisfies lint checks when using with defer and you don't care if there
    68  // is an error, so instead of:
    69  //
    70  //	defer func() { _ = DiscardAndCloseBody(resp) }()
    71  //	defer DiscardAndCloseBodyIgnoreError(resp)
    72  func DiscardAndCloseBodyIgnoreError(resp *http.Response) {
    73  	_ = DiscardAndCloseBody(resp)
    74  }
    75  
    76  // parseURL ensures error if parse error or no url was returned from url.Parse
    77  func parseURL(urlString string) (*url.URL, error) {
    78  	url, parseErr := url.Parse(urlString)
    79  	if parseErr != nil {
    80  		return nil, parseErr
    81  	}
    82  	if url == nil {
    83  		return nil, fmt.Errorf("No URL")
    84  	}
    85  	return url, nil
    86  }
    87  
    88  // URLExists returns error if URL doesn't exist
    89  func URLExists(urlString string, timeout time.Duration, log Log) (bool, error) {
    90  	url, err := parseURL(urlString)
    91  	if err != nil {
    92  		return false, err
    93  	}
    94  
    95  	// Handle local files
    96  	if url.Scheme == "file" {
    97  		return FileExists(PathFromURL(url))
    98  	}
    99  
   100  	log.Debugf("Checking URL exists: %s", urlString)
   101  	req, err := http.NewRequest("HEAD", urlString, nil)
   102  	if err != nil {
   103  		return false, err
   104  	}
   105  	client := &http.Client{
   106  		Timeout: timeout,
   107  	}
   108  	resp, requestErr := client.Do(req)
   109  	if requestErr != nil {
   110  		return false, requestErr
   111  	}
   112  	if resp == nil {
   113  		return false, fmt.Errorf("No response")
   114  	}
   115  	defer DiscardAndCloseBodyIgnoreError(resp)
   116  	if resp.StatusCode != http.StatusOK {
   117  		return false, fmt.Errorf("Invalid status code (%d)", resp.StatusCode)
   118  	}
   119  	return true, nil
   120  }
   121  
   122  // DownloadURLOptions are options for DownloadURL
   123  type DownloadURLOptions struct {
   124  	Digest        string
   125  	RequireDigest bool
   126  	UseETag       bool
   127  	Timeout       time.Duration
   128  	Log           Log
   129  }
   130  
   131  // DownloadURL downloads a URL to a path.
   132  func DownloadURL(urlString string, destinationPath string, options DownloadURLOptions) error {
   133  	_, err := downloadURL(urlString, destinationPath, options)
   134  	return err
   135  }
   136  
   137  func downloadURL(urlString string, destinationPath string, options DownloadURLOptions) (cached bool, _ error) {
   138  	log := options.Log
   139  
   140  	url, err := parseURL(urlString)
   141  	if err != nil {
   142  		return false, err
   143  	}
   144  
   145  	// Handle local files
   146  	if url.Scheme == fileScheme {
   147  		return cached, downloadLocal(PathFromURL(url), destinationPath, options)
   148  	}
   149  
   150  	// Compute ETag if the destinationPath already exists
   151  	etag := ""
   152  	if options.UseETag {
   153  		if _, statErr := os.Stat(destinationPath); statErr == nil {
   154  			computedEtag, etagErr := ComputeEtag(destinationPath)
   155  			if etagErr != nil {
   156  				log.Warningf("Error computing etag", etagErr)
   157  			} else {
   158  				etag = computedEtag
   159  			}
   160  		}
   161  	}
   162  
   163  	req, err := http.NewRequest("GET", url.String(), nil)
   164  	if err != nil {
   165  		return cached, err
   166  	}
   167  	if etag != "" {
   168  		log.Infof("Using etag: %s", etag)
   169  		req.Header.Set("If-None-Match", etag)
   170  	}
   171  	var client http.Client
   172  	if options.Timeout > 0 {
   173  		client = http.Client{Timeout: options.Timeout}
   174  	} else {
   175  		client = http.Client{}
   176  	}
   177  	log.Infof("Request %s", url.String())
   178  	resp, requestErr := client.Do(req)
   179  	if requestErr != nil {
   180  		return cached, requestErr
   181  	}
   182  	if resp == nil {
   183  		return cached, fmt.Errorf("No response")
   184  	}
   185  	defer DiscardAndCloseBodyIgnoreError(resp)
   186  	if resp.StatusCode == http.StatusNotModified {
   187  		cached = true
   188  		// ETag matched, we already have it
   189  		log.Infof("Using cached file: %s", destinationPath)
   190  		return cached, nil
   191  	}
   192  	if resp.StatusCode != http.StatusOK {
   193  		return cached, fmt.Errorf("Responded with %s", resp.Status)
   194  	}
   195  
   196  	savePath := fmt.Sprintf("%s.download", destinationPath)
   197  	if _, ferr := os.Stat(savePath); ferr == nil {
   198  		log.Infof("Removing existing partial download: %s", savePath)
   199  		if rerr := os.Remove(savePath); rerr != nil {
   200  			return cached, fmt.Errorf("Error removing existing partial download: %s", rerr)
   201  		}
   202  	}
   203  
   204  	if err := MakeParentDirs(savePath, 0700, log); err != nil {
   205  		return cached, err
   206  	}
   207  
   208  	if err := SaveHTTPResponse(resp, savePath, 0600, log); err != nil {
   209  		return cached, err
   210  	}
   211  
   212  	if options.RequireDigest {
   213  		if err := CheckDigest(options.Digest, savePath, log); err != nil {
   214  			return cached, err
   215  		}
   216  	}
   217  
   218  	if err := MoveFile(savePath, destinationPath, "", log); err != nil {
   219  		return cached, err
   220  	}
   221  
   222  	return cached, nil
   223  }
   224  
   225  func downloadLocal(localPath string, destinationPath string, options DownloadURLOptions) error {
   226  	if err := CopyFile(localPath, destinationPath, options.Log); err != nil {
   227  		return err
   228  	}
   229  
   230  	if options.RequireDigest {
   231  		if err := CheckDigest(options.Digest, destinationPath, options.Log); err != nil {
   232  			return err
   233  		}
   234  	}
   235  	return nil
   236  }
   237  
   238  // URLValueForBool returns "1" for true, otherwise "0"
   239  func URLValueForBool(b bool) string {
   240  	if b {
   241  		return "1"
   242  	}
   243  	return "0"
   244  }