github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/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 }