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  }