github.com/zaolin/u-root@v0.0.0-20200428085104-64aaafd46c6d/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  	"errors"
    12  	"fmt"
    13  	"io"
    14  	"log"
    15  	"net/http"
    16  	"net/url"
    17  	"os"
    18  	"path/filepath"
    19  	"strings"
    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  // FileSchemeRetryFilter contains extra RetryFilter method for a FileScheme
    48  // wrapped by SchemeWithRetries.
    49  type FileSchemeRetryFilter interface {
    50  	// RetryFilter lets a FileScheme filter for errors returned by Fetch
    51  	// which are worth retrying. If this interface is not implemented, the
    52  	// default for SchemeWithRetries is to always retry. RetryFilter
    53  	// returns true to indicate a request should be retried.
    54  	RetryFilter(u *url.URL, err error) bool
    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  // IsURLError returns true iff err is a URLError.
    87  func IsURLError(err error) bool {
    88  	_, ok := err.(*URLError)
    89  	return ok
    90  }
    91  
    92  // Schemes is a map of URL scheme identifier -> implementation that can
    93  // fetch a file for that scheme.
    94  type Schemes map[string]FileScheme
    95  
    96  // RegisterScheme calls DefaultSchemes.Register.
    97  func RegisterScheme(scheme string, fs FileScheme) {
    98  	DefaultSchemes.Register(scheme, fs)
    99  }
   100  
   101  // Register registers a scheme identified by `scheme` to be `fs`.
   102  func (s Schemes) Register(scheme string, fs FileScheme) {
   103  	s[scheme] = fs
   104  }
   105  
   106  // Fetch fetchs a file via DefaultSchemes.
   107  func Fetch(u *url.URL) (io.ReaderAt, error) {
   108  	return DefaultSchemes.Fetch(u)
   109  }
   110  
   111  // file is an io.ReaderAt with a nice Stringer.
   112  type file struct {
   113  	io.ReaderAt
   114  
   115  	url *url.URL
   116  }
   117  
   118  // String implements fmt.Stringer.
   119  func (f file) String() string {
   120  	return f.url.String()
   121  }
   122  
   123  // Fetch fetchs the file with the given `u`. `u.Scheme` is used to
   124  // select the FileScheme via `s`.
   125  //
   126  // If `s` does not contain a FileScheme for `u.Scheme`, ErrNoSuchScheme is
   127  // returned.
   128  func (s Schemes) Fetch(u *url.URL) (io.ReaderAt, error) {
   129  	fg, ok := s[u.Scheme]
   130  	if !ok {
   131  		return nil, &URLError{URL: u, Err: ErrNoSuchScheme}
   132  	}
   133  	r, err := fg.Fetch(u)
   134  	if err != nil {
   135  		return nil, &URLError{URL: u, Err: err}
   136  	}
   137  	return &file{ReaderAt: r, url: u}, nil
   138  }
   139  
   140  // RetryFilter implements FileSchemeRetryFilter.
   141  func (s Schemes) RetryFilter(u *url.URL, err error) bool {
   142  	fg, ok := s[u.Scheme]
   143  	if !ok {
   144  		return false
   145  	}
   146  	if fg, ok := fg.(FileSchemeRetryFilter); ok {
   147  		return fg.RetryFilter(u, err)
   148  	}
   149  	return true
   150  }
   151  
   152  // LazyFetch calls LazyFetch on DefaultSchemes.
   153  func LazyFetch(u *url.URL) (io.ReaderAt, error) {
   154  	return DefaultSchemes.LazyFetch(u)
   155  }
   156  
   157  // LazyFetch returns a reader that will Fetch the file given by `u` when
   158  // Read is called, based on `u`s scheme. See Schemes.Fetch for more
   159  // details.
   160  func (s Schemes) LazyFetch(u *url.URL) (io.ReaderAt, error) {
   161  	fg, ok := s[u.Scheme]
   162  	if !ok {
   163  		return nil, &URLError{URL: u, Err: ErrNoSuchScheme}
   164  	}
   165  
   166  	return &file{
   167  		url: u,
   168  		ReaderAt: uio.NewLazyOpenerAt(u.String(), func() (io.ReaderAt, error) {
   169  			r, err := fg.Fetch(u)
   170  			if err != nil {
   171  				return nil, &URLError{URL: u, Err: err}
   172  			}
   173  			return r, nil
   174  		}),
   175  	}, nil
   176  }
   177  
   178  // TFTPClient implements FileScheme for TFTP files.
   179  type TFTPClient struct {
   180  	opts []tftp.ClientOpt
   181  }
   182  
   183  // NewTFTPClient returns a new TFTP client based on the given tftp.ClientOpt.
   184  func NewTFTPClient(opts ...tftp.ClientOpt) FileScheme {
   185  	return &TFTPClient{
   186  		opts: opts,
   187  	}
   188  }
   189  
   190  // Fetch implements FileScheme.Fetch.
   191  func (t *TFTPClient) Fetch(u *url.URL) (io.ReaderAt, error) {
   192  	// TODO(hugelgupf): These clients are basically stateless, except for
   193  	// the options. Figure out whether you actually have to re-establish
   194  	// this connection every time. Audit the TFTP library.
   195  	c, err := tftp.NewClient(t.opts...)
   196  	if err != nil {
   197  		return nil, err
   198  	}
   199  
   200  	r, err := c.Get(u.String())
   201  	if err != nil {
   202  		return nil, err
   203  	}
   204  	return uio.NewCachingReader(r), nil
   205  }
   206  
   207  // RetryFilter implements FileSchemeRetryFilter.
   208  func (t *TFTPClient) RetryFilter(u *url.URL, err error) bool {
   209  	// The tftp does not export the necessary structs to get the
   210  	// code out of the error message cleanly.
   211  	return !strings.Contains(err.Error(), "FILE_NOT_FOUND")
   212  }
   213  
   214  // SchemeWithRetries wraps a FileScheme and automatically retries (with
   215  // backoff) when Fetch returns a non-nil err.
   216  type SchemeWithRetries struct {
   217  	Scheme  FileScheme
   218  	BackOff backoff.BackOff
   219  }
   220  
   221  // Fetch implements FileScheme.Fetch.
   222  func (s *SchemeWithRetries) Fetch(u *url.URL) (io.ReaderAt, error) {
   223  	var err error
   224  	s.BackOff.Reset()
   225  	for d := time.Duration(0); d != backoff.Stop; d = s.BackOff.NextBackOff() {
   226  		if d > 0 {
   227  			time.Sleep(d)
   228  		}
   229  
   230  		var r io.ReaderAt
   231  		// Note: err uses the scope outside the for loop.
   232  		r, err = s.Scheme.Fetch(u)
   233  		if err == nil {
   234  			return r, nil
   235  		}
   236  
   237  		log.Printf("Error: Getting %v: %v", u, err)
   238  		if s, ok := s.Scheme.(FileSchemeRetryFilter); ok && !s.RetryFilter(u, err) {
   239  			return r, err
   240  		}
   241  		log.Printf("Retrying %v", u)
   242  	}
   243  
   244  	log.Printf("Error: Too many retries to get file %v", u)
   245  	return nil, err
   246  }
   247  
   248  // HTTPClientCodeError is returned by HTTPClient.Fetch when the server replies
   249  // with a non-200 code.
   250  type HTTPClientCodeError struct {
   251  	Err      error
   252  	HTTPCode int
   253  }
   254  
   255  // Error implements error for HTTPClientCodeError.
   256  func (h *HTTPClientCodeError) Error() string {
   257  	return fmt.Sprintf("HTTP server responded with error code %d, want 200: response %v", h.HTTPCode, h.Err)
   258  }
   259  
   260  // HTTPClient implements FileScheme for HTTP files.
   261  type HTTPClient struct {
   262  	c *http.Client
   263  }
   264  
   265  // NewHTTPClient returns a new HTTP FileScheme based on the given http.Client.
   266  func NewHTTPClient(c *http.Client) *HTTPClient {
   267  	return &HTTPClient{
   268  		c: c,
   269  	}
   270  }
   271  
   272  // Fetch implements FileScheme.Fetch.
   273  func (h HTTPClient) Fetch(u *url.URL) (io.ReaderAt, error) {
   274  	resp, err := h.c.Get(u.String())
   275  	if err != nil {
   276  		return nil, err
   277  	}
   278  
   279  	if resp.StatusCode != 200 {
   280  		return nil, &HTTPClientCodeError{err, resp.StatusCode}
   281  	}
   282  	return uio.NewCachingReader(resp.Body), nil
   283  }
   284  
   285  // RetryFilter implements FileSchemeRetryFilter.
   286  func (h HTTPClient) RetryFilter(u *url.URL, err error) bool {
   287  	if err, ok := err.(*HTTPClientCodeError); ok && err.HTTPCode == 200 {
   288  		return false
   289  	}
   290  	return true
   291  }
   292  
   293  // LocalFileClient implements FileScheme for files on disk.
   294  type LocalFileClient struct{}
   295  
   296  // Fetch implements FileScheme.Fetch.
   297  func (lfs LocalFileClient) Fetch(u *url.URL) (io.ReaderAt, error) {
   298  	return os.Open(filepath.Clean(u.Path))
   299  }