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  }