github.com/hugelgupf/u-root@v0.0.0-20191023214958-4807c632154c/pkg/urlfetch/schemes.go (about) 1 // Copyright 2017-2018 the u-root Authors. All rights reserved 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 // Package urlfetch implements routines to fetch files given a URL. 6 // 7 // urlfetch currently supports HTTP, TFTP, local files, and a retrying HTTP 8 // client. 9 package urlfetch 10 11 import ( 12 "errors" 13 "fmt" 14 "io" 15 "log" 16 "net/http" 17 "net/url" 18 "os" 19 "path/filepath" 20 "time" 21 22 "github.com/cenkalti/backoff" 23 "github.com/u-root/u-root/pkg/uio" 24 "pack.ag/tftp" 25 ) 26 27 var ( 28 // ErrNoSuchScheme is returned by Schemes.Fetch and 29 // Schemes.LazyFetch if there is no registered FileScheme 30 // implementation for the given URL scheme. 31 ErrNoSuchScheme = errors.New("no such scheme") 32 ) 33 34 // FileScheme represents the implementation of a URL scheme and gives access to 35 // fetching files of that scheme. 36 // 37 // For example, an http FileScheme implementation would fetch files using 38 // the HTTP protocol. 39 type FileScheme interface { 40 // Fetch returns a reader that gives the contents of `u`. 41 // 42 // It may do so by fetching `u` and placing it in a buffer, or by 43 // returning an io.ReaderAt that fetchs the file. 44 Fetch(u *url.URL) (io.ReaderAt, error) 45 } 46 47 var ( 48 // DefaultHTTPClient is the default HTTP FileScheme. 49 // 50 // It is not recommended to use this for HTTPS. We recommend creating an 51 // http.Client that accepts only a private pool of certificates. 52 DefaultHTTPClient = NewHTTPClient(http.DefaultClient) 53 54 // DefaultTFTPClient is the default TFTP FileScheme. 55 DefaultTFTPClient = NewTFTPClient(tftp.ClientMode(tftp.ModeOctet), tftp.ClientBlocksize(1450), tftp.ClientWindowsize(65535)) 56 57 // DefaultSchemes are the schemes supported by default. 58 DefaultSchemes = Schemes{ 59 "tftp": DefaultTFTPClient, 60 "http": DefaultHTTPClient, 61 "file": &LocalFileClient{}, 62 } 63 ) 64 65 // URLError is an error involving URLs. 66 type URLError struct { 67 URL *url.URL 68 Err error 69 } 70 71 // Error implements error.Error. 72 func (s *URLError) Error() string { 73 return fmt.Sprintf("encountered error %v with %q", s.Err, s.URL) 74 } 75 76 // IsURLError returns true iff err is a URLError. 77 func IsURLError(err error) bool { 78 _, ok := err.(*URLError) 79 return ok 80 } 81 82 // Schemes is a map of URL scheme identifier -> implementation that can 83 // fetch a file for that scheme. 84 type Schemes map[string]FileScheme 85 86 // RegisterScheme calls DefaultSchemes.Register. 87 func RegisterScheme(scheme string, fs FileScheme) { 88 DefaultSchemes.Register(scheme, fs) 89 } 90 91 // Register registers a scheme identified by `scheme` to be `fs`. 92 func (s Schemes) Register(scheme string, fs FileScheme) { 93 s[scheme] = fs 94 } 95 96 // Fetch fetchs a file via DefaultSchemes. 97 func Fetch(u *url.URL) (io.ReaderAt, error) { 98 return DefaultSchemes.Fetch(u) 99 } 100 101 // file is an io.ReaderAt with a nice Stringer. 102 type file struct { 103 io.ReaderAt 104 105 url *url.URL 106 } 107 108 // String implements fmt.Stringer. 109 func (f file) String() string { 110 return f.url.String() 111 } 112 113 // Fetch fetchs the file with the given `u`. `u.Scheme` is used to 114 // select the FileScheme via `s`. 115 // 116 // If `s` does not contain a FileScheme for `u.Scheme`, ErrNoSuchScheme is 117 // returned. 118 func (s Schemes) Fetch(u *url.URL) (io.ReaderAt, error) { 119 fg, ok := s[u.Scheme] 120 if !ok { 121 return nil, &URLError{URL: u, Err: ErrNoSuchScheme} 122 } 123 r, err := fg.Fetch(u) 124 if err != nil { 125 return nil, &URLError{URL: u, Err: err} 126 } 127 return &file{ReaderAt: r, url: u}, nil 128 } 129 130 // LazyFetch calls LazyFetch on DefaultSchemes. 131 func LazyFetch(u *url.URL) (io.ReaderAt, error) { 132 return DefaultSchemes.LazyFetch(u) 133 } 134 135 // LazyFetch returns a reader that will Fetch the file given by `u` when 136 // Read is called, based on `u`s scheme. See Schemes.Fetch for more 137 // details. 138 func (s Schemes) LazyFetch(u *url.URL) (io.ReaderAt, error) { 139 fg, ok := s[u.Scheme] 140 if !ok { 141 return nil, &URLError{URL: u, Err: ErrNoSuchScheme} 142 } 143 144 return &file{ 145 url: u, 146 ReaderAt: uio.NewLazyOpenerAt(func() (io.ReaderAt, error) { 147 r, err := fg.Fetch(u) 148 if err != nil { 149 return nil, &URLError{URL: u, Err: err} 150 } 151 return r, nil 152 }), 153 }, nil 154 } 155 156 // TFTPClient implements FileScheme for TFTP files. 157 type TFTPClient struct { 158 opts []tftp.ClientOpt 159 } 160 161 // NewTFTPClient returns a new TFTP client based on the given tftp.ClientOpt. 162 func NewTFTPClient(opts ...tftp.ClientOpt) FileScheme { 163 return &TFTPClient{ 164 opts: opts, 165 } 166 } 167 168 // Fetch implements FileScheme.Fetch. 169 func (t *TFTPClient) Fetch(u *url.URL) (io.ReaderAt, error) { 170 // TODO(hugelgupf): These clients are basically stateless, except for 171 // the options. Figure out whether you actually have to re-establish 172 // this connection every time. Audit the TFTP library. 173 c, err := tftp.NewClient(t.opts...) 174 if err != nil { 175 return nil, err 176 } 177 178 r, err := c.Get(u.String()) 179 if err != nil { 180 return nil, err 181 } 182 return uio.NewCachingReader(r), nil 183 } 184 185 // SchemeWithRetries wraps a FileScheme and automatically retries (with 186 // backoff) when Fetch returns a non-nil err. 187 type SchemeWithRetries struct { 188 Scheme FileScheme 189 BackOff backoff.BackOff 190 } 191 192 // Fetch implements FileScheme.Fetch. 193 func (s *SchemeWithRetries) Fetch(u *url.URL) (io.ReaderAt, error) { 194 var err error 195 s.BackOff.Reset() 196 for d := time.Duration(0); d != backoff.Stop; d = s.BackOff.NextBackOff() { 197 if d > 0 { 198 time.Sleep(d) 199 } 200 201 var r io.ReaderAt 202 r, err = s.Scheme.Fetch(u) 203 if err != nil { 204 log.Printf("Error: Getting %v: %v", u, err) 205 continue 206 } 207 return r, nil 208 } 209 210 log.Printf("Error: Too many retries to get file %v", u) 211 return nil, err 212 } 213 214 // HTTPClient implements FileScheme for HTTP files. 215 type HTTPClient struct { 216 c *http.Client 217 } 218 219 // NewHTTPClient returns a new HTTP FileScheme based on the given http.Client. 220 func NewHTTPClient(c *http.Client) *HTTPClient { 221 return &HTTPClient{ 222 c: c, 223 } 224 } 225 226 // Fetch implements FileScheme.Fetch. 227 func (h HTTPClient) Fetch(u *url.URL) (io.ReaderAt, error) { 228 resp, err := h.c.Get(u.String()) 229 if err != nil { 230 return nil, err 231 } 232 233 if resp.StatusCode != 200 { 234 return nil, fmt.Errorf("HTTP server responded with code %d, want 200: response %v", resp.StatusCode, resp) 235 } 236 return uio.NewCachingReader(resp.Body), nil 237 } 238 239 // HTTPClientWithRetries implements FileScheme for HTTP files and automatically 240 // retries (with backoff) upon an error. 241 type HTTPClientWithRetries struct { 242 Client *http.Client 243 BackOff backoff.BackOff 244 } 245 246 // Fetch implements FileScheme.Fetch. 247 func (h HTTPClientWithRetries) Fetch(u *url.URL) (io.ReaderAt, error) { 248 req, err := http.NewRequest("GET", u.String(), nil) 249 if err != nil { 250 return nil, err 251 } 252 253 h.BackOff.Reset() 254 for d := time.Duration(0); d != backoff.Stop; d = h.BackOff.NextBackOff() { 255 if d > 0 { 256 time.Sleep(d) 257 } 258 259 var resp *http.Response 260 // Note: err uses the scope outside the for loop. 261 resp, err = h.Client.Do(req) 262 if err != nil { 263 log.Printf("Error: HTTP client: %v", err) 264 continue 265 } 266 if resp.StatusCode != 200 { 267 log.Printf("Error: HTTP server responded with code %d, want 200: response %v", resp.StatusCode, resp) 268 continue 269 } 270 return uio.NewCachingReader(resp.Body), nil 271 } 272 log.Printf("Error: Too many retries to download %v", u) 273 return nil, fmt.Errorf("too many HTTP retries: %v", err) 274 } 275 276 // LocalFileClient implements FileScheme for files on disk. 277 type LocalFileClient struct{} 278 279 // Fetch implements FileScheme.Fetch. 280 func (lfs LocalFileClient) Fetch(u *url.URL) (io.ReaderAt, error) { 281 return os.Open(filepath.Clean(u.Path)) 282 }