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