github.com/u-root/u-root@v7.0.1-0.20200915234505-ad7babab0a8e+incompatible/pkg/curl/schemes.go (about) 1 // Copyright 2017-2020 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 curl implements routines to fetch files given a URL. 6 // 7 // curl currently supports HTTP, TFTP, and local files. 8 package curl 9 10 import ( 11 "context" 12 "errors" 13 "fmt" 14 "io" 15 "log" 16 "net" 17 "net/http" 18 "net/url" 19 "os" 20 "path/filepath" 21 "strings" 22 "time" 23 24 "github.com/cenkalti/backoff" 25 "github.com/u-root/u-root/pkg/uio" 26 "pack.ag/tftp" 27 ) 28 29 var ( 30 // ErrNoSuchScheme is returned by Schemes.Fetch and 31 // Schemes.LazyFetch if there is no registered FileScheme 32 // implementation for the given URL scheme. 33 ErrNoSuchScheme = errors.New("no such scheme") 34 ) 35 36 // File is a reference to a file fetched through this library. 37 type File interface { 38 io.ReaderAt 39 40 // URL is the file's original URL. 41 URL() *url.URL 42 } 43 44 // FileScheme represents the implementation of a URL scheme and gives access to 45 // fetching files of that scheme. 46 // 47 // For example, an http FileScheme implementation would fetch files using 48 // the HTTP protocol. 49 type FileScheme interface { 50 // Fetch returns a reader that gives the contents of `u`. 51 // 52 // It may do so by fetching `u` and placing it in a buffer, or by 53 // returning an io.ReaderAt that fetchs the file. 54 Fetch(ctx context.Context, u *url.URL) (io.ReaderAt, error) 55 } 56 57 var ( 58 // DefaultHTTPClient is the default HTTP FileScheme. 59 // 60 // It is not recommended to use this for HTTPS. We recommend creating an 61 // http.Client that accepts only a private pool of certificates. 62 DefaultHTTPClient = NewHTTPClient(http.DefaultClient) 63 64 // DefaultTFTPClient is the default TFTP FileScheme. 65 DefaultTFTPClient = NewTFTPClient(tftp.ClientMode(tftp.ModeOctet), tftp.ClientBlocksize(1450), tftp.ClientWindowsize(65535)) 66 67 // DefaultSchemes are the schemes supported by default. 68 DefaultSchemes = Schemes{ 69 "tftp": DefaultTFTPClient, 70 "http": DefaultHTTPClient, 71 "file": &LocalFileClient{}, 72 } 73 ) 74 75 // URLError is an error involving URLs. 76 type URLError struct { 77 URL *url.URL 78 Err error 79 } 80 81 // Error implements error.Error. 82 func (s *URLError) Error() string { 83 return fmt.Sprintf("encountered error %v with %q", s.Err, s.URL) 84 } 85 86 // Unwrap unwraps the underlying error. 87 func (s *URLError) Unwrap() error { 88 return s.Err 89 } 90 91 // IsURLError returns true iff err is a URLError. 92 func IsURLError(err error) bool { 93 _, ok := err.(*URLError) 94 return ok 95 } 96 97 // Schemes is a map of URL scheme identifier -> implementation that can 98 // fetch a file for that scheme. 99 type Schemes map[string]FileScheme 100 101 // RegisterScheme calls DefaultSchemes.Register. 102 func RegisterScheme(scheme string, fs FileScheme) { 103 DefaultSchemes.Register(scheme, fs) 104 } 105 106 // Register registers a scheme identified by `scheme` to be `fs`. 107 func (s Schemes) Register(scheme string, fs FileScheme) { 108 s[scheme] = fs 109 } 110 111 // Fetch fetchs a file via DefaultSchemes. 112 func Fetch(ctx context.Context, u *url.URL) (File, error) { 113 return DefaultSchemes.Fetch(ctx, u) 114 } 115 116 // file is an io.ReaderAt with a nice Stringer. 117 type file struct { 118 io.ReaderAt 119 120 url *url.URL 121 } 122 123 // URL returns the file URL. 124 func (f file) URL() *url.URL { 125 return f.url 126 } 127 128 // String implements fmt.Stringer. 129 func (f file) String() string { 130 return f.url.String() 131 } 132 133 // Fetch fetchs the file with the given `u`. `u.Scheme` is used to 134 // select the FileScheme via `s`. 135 // 136 // If `s` does not contain a FileScheme for `u.Scheme`, ErrNoSuchScheme is 137 // returned. 138 func (s Schemes) Fetch(ctx context.Context, u *url.URL) (File, error) { 139 fg, ok := s[u.Scheme] 140 if !ok { 141 return nil, &URLError{URL: u, Err: ErrNoSuchScheme} 142 } 143 r, err := fg.Fetch(ctx, u) 144 if err != nil { 145 return nil, &URLError{URL: u, Err: err} 146 } 147 return &file{ReaderAt: r, url: u}, nil 148 } 149 150 // LazyFetch calls LazyFetch on DefaultSchemes. 151 func LazyFetch(u *url.URL) (File, error) { 152 return DefaultSchemes.LazyFetch(u) 153 } 154 155 // LazyFetch returns a reader that will Fetch the file given by `u` when 156 // Read is called, based on `u`s scheme. See Schemes.Fetch for more 157 // details. 158 func (s Schemes) LazyFetch(u *url.URL) (File, error) { 159 fg, ok := s[u.Scheme] 160 if !ok { 161 return nil, &URLError{URL: u, Err: ErrNoSuchScheme} 162 } 163 164 return &file{ 165 url: u, 166 ReaderAt: uio.NewLazyOpenerAt(u.String(), func() (io.ReaderAt, error) { 167 // TODO 168 r, err := fg.Fetch(context.TODO(), u) 169 if err != nil { 170 return nil, &URLError{URL: u, Err: err} 171 } 172 return r, nil 173 }), 174 }, nil 175 } 176 177 // TFTPClient implements FileScheme for TFTP files. 178 type TFTPClient struct { 179 opts []tftp.ClientOpt 180 } 181 182 // NewTFTPClient returns a new TFTP client based on the given tftp.ClientOpt. 183 func NewTFTPClient(opts ...tftp.ClientOpt) FileScheme { 184 return &TFTPClient{ 185 opts: opts, 186 } 187 } 188 189 // Fetch implements FileScheme.Fetch. 190 func (t *TFTPClient) Fetch(_ context.Context, u *url.URL) (io.ReaderAt, error) { 191 // TODO(hugelgupf): These clients are basically stateless, except for 192 // the options. Figure out whether you actually have to re-establish 193 // this connection every time. Audit the TFTP library. 194 c, err := tftp.NewClient(t.opts...) 195 if err != nil { 196 return nil, err 197 } 198 199 r, err := c.Get(u.String()) 200 if err != nil { 201 return nil, err 202 } 203 return uio.NewCachingReader(r), nil 204 } 205 206 // RetryTFTP retries downloads if the error does not contain FILE_NOT_FOUND. 207 // 208 // pack.ag/tftp does not export the necessary structs to get the 209 // code out of the error message cleanly, but it does embed FILE_NOT_FOUND in 210 // the error string. 211 func RetryTFTP(u *url.URL, err error) bool { 212 return !strings.Contains(err.Error(), "FILE_NOT_FOUND") 213 } 214 215 // DoRetry returns true if the Fetch request for the URL should be 216 // retried. err is the error that Fetch previously returned. 217 // 218 // DoRetry lets a FileScheme filter for errors returned by Fetch 219 // which are worth retrying. If this interface is not implemented, the 220 // default for SchemeWithRetries is to always retry. DoRetry 221 // returns true to indicate a request should be retried. 222 type DoRetry func(u *url.URL, err error) bool 223 224 // SchemeWithRetries wraps a FileScheme and automatically retries (with 225 // backoff) when Fetch returns a non-nil err. 226 type SchemeWithRetries struct { 227 Scheme FileScheme 228 229 // DoRetry should return true to indicate the Fetch shall be retried. 230 // Even if DoRetry returns true, BackOff can still determine whether to 231 // stop. 232 // 233 // If DoRetry is nil, it will be retried if the BackOff agrees. 234 DoRetry DoRetry 235 236 // BackOff determines how often to retry and how long to wait between 237 // each retry. 238 BackOff backoff.BackOff 239 } 240 241 // Fetch implements FileScheme.Fetch. 242 func (s *SchemeWithRetries) Fetch(ctx context.Context, u *url.URL) (io.ReaderAt, error) { 243 var err error 244 s.BackOff.Reset() 245 back := backoff.WithContext(s.BackOff, ctx) 246 for d := time.Duration(0); d != backoff.Stop; d = back.NextBackOff() { 247 if d > 0 { 248 time.Sleep(d) 249 } 250 251 var r io.ReaderAt 252 // Note: err uses the scope outside the for loop. 253 r, err = s.Scheme.Fetch(ctx, u) 254 if err == nil { 255 return r, nil 256 } 257 258 log.Printf("Error: Getting %v: %v", u, err) 259 if s.DoRetry != nil && !s.DoRetry(u, err) { 260 return r, err 261 } 262 log.Printf("Retrying %v", u) 263 } 264 265 log.Printf("Error: Too many retries to get file %v", u) 266 return nil, err 267 } 268 269 // HTTPClientCodeError is returned by HTTPClient.Fetch when the server replies 270 // with a non-200 code. 271 type HTTPClientCodeError struct { 272 Err error 273 HTTPCode int 274 } 275 276 // Error implements error for HTTPClientCodeError. 277 func (h *HTTPClientCodeError) Error() string { 278 return fmt.Sprintf("HTTP server responded with error code %d, want 200: response %v", h.HTTPCode, h.Err) 279 } 280 281 // Unwrap implements errors.Unwrap. 282 func (h *HTTPClientCodeError) Unwrap() error { 283 return h.Err 284 } 285 286 // HTTPClient implements FileScheme for HTTP files. 287 type HTTPClient struct { 288 c *http.Client 289 } 290 291 // NewHTTPClient returns a new HTTP FileScheme based on the given http.Client. 292 func NewHTTPClient(c *http.Client) *HTTPClient { 293 return &HTTPClient{ 294 c: c, 295 } 296 } 297 298 // Fetch implements FileScheme.Fetch. 299 func (h HTTPClient) Fetch(ctx context.Context, u *url.URL) (io.ReaderAt, error) { 300 req, err := http.NewRequestWithContext(ctx, "GET", u.String(), nil) 301 if err != nil { 302 return nil, err 303 } 304 resp, err := h.c.Do(req) 305 if err != nil { 306 return nil, err 307 } 308 309 if resp.StatusCode != 200 { 310 return nil, &HTTPClientCodeError{err, resp.StatusCode} 311 } 312 return uio.NewCachingReader(resp.Body), nil 313 } 314 315 // RetryOr returns a DoRetry function that returns true if any one of fn return 316 // true. 317 func RetryOr(fn ...DoRetry) DoRetry { 318 return func(u *url.URL, err error) bool { 319 for _, f := range fn { 320 if f(u, err) { 321 return true 322 } 323 } 324 return false 325 } 326 } 327 328 // RetryConnectErrors retries only connect(2) errors. 329 func RetryConnectErrors(u *url.URL, err error) bool { 330 var serr *os.SyscallError 331 if errors.As(err, &serr) && serr.Syscall == "connect" { 332 return true 333 } 334 return false 335 } 336 337 // RetryTemporaryNetworkErrors only retries temporary network errors. 338 // 339 // This relies on Go's net.Error.Temporary definition of temporary network 340 // errors, which does not include network configuration errors. The latter are 341 // relevant for users of DHCP, for example. 342 func RetryTemporaryNetworkErrors(u *url.URL, err error) bool { 343 var nerr net.Error 344 if errors.As(err, &nerr) { 345 return nerr.Temporary() 346 } 347 return false 348 } 349 350 // RetryHTTP implements DoRetry for HTTP error codes where it makes sense. 351 func RetryHTTP(u *url.URL, err error) bool { 352 var e *HTTPClientCodeError 353 if !errors.As(err, &e) { 354 return false 355 } 356 switch c := e.HTTPCode; { 357 case c == 200: 358 return false 359 360 case c == 408, c == 409, c == 425, c == 429: 361 // Retry for codes "Request Timeout(408), Conflict(409), Too Early(425), and Too Many Requests(429)" 362 return true 363 364 case c >= 400 && c < 500: 365 // We don't retry all other 400 codes, since the situation won't be improved with a retry. 366 return false 367 368 default: 369 return true 370 } 371 } 372 373 // LocalFileClient implements FileScheme for files on disk. 374 type LocalFileClient struct{} 375 376 // Fetch implements FileScheme.Fetch. 377 func (lfs LocalFileClient) Fetch(_ context.Context, u *url.URL) (io.ReaderAt, error) { 378 return os.Open(filepath.Clean(u.Path)) 379 }