@@ -23,13 +23,15 @@ import (
2323 "net"
2424 "net/http"
2525 "os"
26- "path"
26+ "path/filepath "
2727 "time"
2828
2929 "k8s.io/klog/v2"
3030 "k8s.io/kops/util/pkg/hashing"
3131)
3232
33+ const downloadTimeout = 3 * time .Minute
34+
3335// DownloadURL will download the file at the given url and store it as dest.
3436// If hash is non-nil, it will also verify that it matches the hash of the downloaded file.
3537func DownloadURL (url string , dest string , hash * hashing.Hash ) (* hashing.Hash , error ) {
@@ -43,47 +45,103 @@ func DownloadURL(url string, dest string, hash *hashing.Hash) (*hashing.Hash, er
4345 }
4446 }
4547
46- dirMode := os .FileMode (0o755 )
47- err := downloadURLAlways (url , dest , dirMode )
48+ return downloadURLToFile (url , dest , hash )
49+ }
50+
51+ func downloadURLToFile (url string , destPath string , hash * hashing.Hash ) (* hashing.Hash , error ) {
52+ dir := filepath .Dir (destPath )
53+ if err := os .MkdirAll (dir , 0o755 ); err != nil {
54+ return nil , fmt .Errorf ("error creating directories for destination file %q: %v" , destPath , err )
55+ }
56+
57+ output , err := os .CreateTemp (dir , "." + filepath .Base (destPath )+ ".tmp" )
58+ if err != nil {
59+ return nil , fmt .Errorf ("error creating temporary file for download %q: %v" , destPath , err )
60+ }
61+ tempPath := output .Name ()
62+ defer os .Remove (tempPath )
63+
64+ actual , err := downloadURLToWriter (url , output , hash )
65+ if closeErr := output .Close (); closeErr != nil && err == nil {
66+ err = closeErr
67+ }
4868 if err != nil {
4969 return nil , err
5070 }
71+ if err := os .Chmod (tempPath , 0o644 ); err != nil {
72+ return nil , fmt .Errorf ("error setting mode on downloaded file %q: %v" , tempPath , err )
73+ }
74+ if err := os .Rename (tempPath , destPath ); err != nil {
75+ return nil , fmt .Errorf ("error moving downloaded file %q to %q: %v" , tempPath , destPath , err )
76+ }
77+ return actual , nil
78+ }
5179
80+ // downloadURLToWriter streams the file at the given url to dest.
81+ // If hash is non-nil, it will also verify that it matches the downloaded bytes.
82+ func downloadURLToWriter (url string , dest io.Writer , hash * hashing.Hash ) (* hashing.Hash , error ) {
83+ responseBody , err := OpenURL (url )
84+ if err != nil {
85+ return nil , err
86+ }
87+ defer responseBody .Close ()
88+
89+ start := time .Now ()
90+ defer func () {
91+ klog .V (2 ).Infof ("Downloading %q took %q" , url , time .Since (start ))
92+ }()
93+ klog .V (2 ).Infof ("Downloading %q" , url )
94+
95+ algorithm := hashing .HashAlgorithmSHA256
5296 if hash != nil {
53- match , err := fileHasHash (dest , hash )
54- if err != nil {
55- return nil , err
56- }
57- if ! match {
58- return nil , fmt .Errorf ("downloaded from %q but hash did not match expected %q" , url , hash )
59- }
60- } else {
61- hash , err = hashing .HashAlgorithmSHA256 .HashFile (dest )
62- if err != nil {
63- return nil , err
64- }
97+ algorithm = hash .Algorithm
6598 }
99+ hasher := algorithm .NewHasher ()
100+ writer := io .MultiWriter (dest , hasher )
66101
67- return hash , nil
102+ if _ , err := io .Copy (writer , responseBody ); err != nil {
103+ return nil , fmt .Errorf ("error downloading HTTP content from %q: %v" , url , err )
104+ }
105+
106+ actual := & hashing.Hash {
107+ Algorithm : algorithm ,
108+ HashValue : hasher .Sum (nil ),
109+ }
110+ if hash != nil && ! actual .Equal (hash ) {
111+ return nil , fmt .Errorf ("downloaded from %q but hash did not match expected %q" , url , hash )
112+ }
113+ return actual , nil
68114}
69115
70- func downloadURLAlways (url string , destPath string , dirMode os.FileMode ) error {
71- err := os .MkdirAll (path .Dir (destPath ), dirMode )
116+ // OpenURL opens a hardened HTTP GET stream for url.
117+ func OpenURL (url string ) (io.ReadCloser , error ) {
118+ httpClient := newDownloadHTTPClient ()
119+
120+ ctx , cancel := context .WithTimeout (context .Background (), downloadTimeout )
121+ req , err := http .NewRequestWithContext (ctx , http .MethodGet , url , nil )
72122 if err != nil {
73- return fmt .Errorf ("error creating directories for destination file %q: %v" , destPath , err )
123+ cancel ()
124+ return nil , fmt .Errorf ("cannot create request: %v" , err )
74125 }
75126
76- output , err := os . Create ( destPath )
127+ response , err := httpClient . Do ( req )
77128 if err != nil {
78- return fmt .Errorf ("error creating file for download %q: %v" , destPath , err )
129+ cancel ()
130+ return nil , fmt .Errorf ("error doing HTTP fetch of %q: %v" , url , err )
79131 }
80- defer output .Close ()
81132
82- klog .V (2 ).Infof ("Downloading %q" , url )
133+ // http.Client follows 3xx automatically, so anything outside 2xx that reaches us is a bug or a missing Location.
134+ if response .StatusCode < 200 || response .StatusCode > 299 {
135+ response .Body .Close ()
136+ cancel ()
137+ return nil , fmt .Errorf ("unexpected response from %q: HTTP %s" , url , response .Status )
138+ }
139+
140+ return & cancelOnCloseReadCloser {ReadCloser : response .Body , cancel : cancel }, nil
141+ }
83142
84- // Create a client with custom timeouts
85- // to avoid idle downloads to hang the program
86- httpClient := & http.Client {
143+ func newDownloadHTTPClient () * http.Client {
144+ return & http.Client {
87145 Transport : & http.Transport {
88146 Proxy : http .ProxyFromEnvironment ,
89147 DialContext : (& net.Dialer {
@@ -95,35 +153,15 @@ func downloadURLAlways(url string, destPath string, dirMode os.FileMode) error {
95153 IdleConnTimeout : 30 * time .Second ,
96154 },
97155 }
156+ }
98157
99- // this will stop slow downloads after 3 minutes
100- // and interrupt reading of the Response.Body
101- ctx , cancel := context .WithTimeout (context .Background (), 3 * time .Minute )
102- defer cancel ()
103-
104- req , err := http .NewRequestWithContext (ctx , http .MethodGet , url , nil )
105- if err != nil {
106- return fmt .Errorf ("Cannot create request: %v" , err )
107- }
108-
109- response , err := httpClient .Do (req )
110- if err != nil {
111- return fmt .Errorf ("error doing HTTP fetch of %q: %v" , url , err )
112- }
113- defer response .Body .Close ()
114-
115- if response .StatusCode >= 400 {
116- return fmt .Errorf ("error response from %q: HTTP %v" , url , response .StatusCode )
117- }
118-
119- start := time .Now ()
120- defer func () {
121- klog .V (2 ).Infof ("Copying %q to %q took %q" , url , destPath , time .Since (start ))
122- }()
158+ type cancelOnCloseReadCloser struct {
159+ io.ReadCloser
160+ cancel context.CancelFunc
161+ }
123162
124- _ , err = io .Copy (output , response .Body )
125- if err != nil {
126- return fmt .Errorf ("error downloading HTTP content from %q: %v" , url , err )
127- }
128- return nil
163+ func (r * cancelOnCloseReadCloser ) Close () error {
164+ err := r .ReadCloser .Close ()
165+ r .cancel ()
166+ return err
129167}
0 commit comments