github.com/xyproto/u-root@v6.0.1-0.20200302025726-5528e0c77a3c+incompatible/pkg/curl/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 curl implements routines to fetch files given a URL.
     6  //
     7  // curl currently supports HTTP, TFTP, local files, and a retrying HTTP client.
     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  	"time"
    20  
    21  	"github.com/cenkalti/backoff"
    22  	"github.com/u-root/u-root/pkg/uio"
    23  	"pack.ag/tftp"
    24  )
    25  
    26  var (
    27  	// ErrNoSuchScheme is returned by Schemes.Fetch and
    28  	// Schemes.LazyFetch if there is no registered FileScheme
    29  	// implementation for the given URL scheme.
    30  	ErrNoSuchScheme = errors.New("no such scheme")
    31  )
    32  
    33  // FileScheme represents the implementation of a URL scheme and gives access to
    34  // fetching files of that scheme.
    35  //
    36  // For example, an http FileScheme implementation would fetch files using
    37  // the HTTP protocol.
    38  type FileScheme interface {
    39  	// Fetch returns a reader that gives the contents of `u`.
    40  	//
    41  	// It may do so by fetching `u` and placing it in a buffer, or by
    42  	// returning an io.ReaderAt that fetchs the file.
    43  	Fetch(u *url.URL) (io.ReaderAt, error)
    44  }
    45  
    46  var (
    47  	// DefaultHTTPClient is the default HTTP FileScheme.
    48  	//
    49  	// It is not recommended to use this for HTTPS. We recommend creating an
    50  	// http.Client that accepts only a private pool of certificates.
    51  	DefaultHTTPClient = NewHTTPClient(http.DefaultClient)
    52  
    53  	// DefaultTFTPClient is the default TFTP FileScheme.
    54  	DefaultTFTPClient = NewTFTPClient(tftp.ClientMode(tftp.ModeOctet), tftp.ClientBlocksize(1450), tftp.ClientWindowsize(65535))
    55  
    56  	// DefaultSchemes are the schemes supported by default.
    57  	DefaultSchemes = Schemes{
    58  		"tftp": DefaultTFTPClient,
    59  		"http": DefaultHTTPClient,
    60  		"file": &LocalFileClient{},
    61  	}
    62  )
    63  
    64  // URLError is an error involving URLs.
    65  type URLError struct {
    66  	URL *url.URL
    67  	Err error
    68  }
    69  
    70  // Error implements error.Error.
    71  func (s *URLError) Error() string {
    72  	return fmt.Sprintf("encountered error %v with %q", s.Err, s.URL)
    73  }
    74  
    75  // IsURLError returns true iff err is a URLError.
    76  func IsURLError(err error) bool {
    77  	_, ok := err.(*URLError)
    78  	return ok
    79  }
    80  
    81  // Schemes is a map of URL scheme identifier -> implementation that can
    82  // fetch a file for that scheme.
    83  type Schemes map[string]FileScheme
    84  
    85  // RegisterScheme calls DefaultSchemes.Register.
    86  func RegisterScheme(scheme string, fs FileScheme) {
    87  	DefaultSchemes.Register(scheme, fs)
    88  }
    89  
    90  // Register registers a scheme identified by `scheme` to be `fs`.
    91  func (s Schemes) Register(scheme string, fs FileScheme) {
    92  	s[scheme] = fs
    93  }
    94  
    95  // Fetch fetchs a file via DefaultSchemes.
    96  func Fetch(u *url.URL) (io.ReaderAt, error) {
    97  	return DefaultSchemes.Fetch(u)
    98  }
    99  
   100  // file is an io.ReaderAt with a nice Stringer.
   101  type file struct {
   102  	io.ReaderAt
   103  
   104  	url *url.URL
   105  }
   106  
   107  // String implements fmt.Stringer.
   108  func (f file) String() string {
   109  	return f.url.String()
   110  }
   111  
   112  // Fetch fetchs the file with the given `u`. `u.Scheme` is used to
   113  // select the FileScheme via `s`.
   114  //
   115  // If `s` does not contain a FileScheme for `u.Scheme`, ErrNoSuchScheme is
   116  // returned.
   117  func (s Schemes) Fetch(u *url.URL) (io.ReaderAt, error) {
   118  	fg, ok := s[u.Scheme]
   119  	if !ok {
   120  		return nil, &URLError{URL: u, Err: ErrNoSuchScheme}
   121  	}
   122  	r, err := fg.Fetch(u)
   123  	if err != nil {
   124  		return nil, &URLError{URL: u, Err: err}
   125  	}
   126  	return &file{ReaderAt: r, url: u}, nil
   127  }
   128  
   129  // LazyFetch calls LazyFetch on DefaultSchemes.
   130  func LazyFetch(u *url.URL) (io.ReaderAt, error) {
   131  	return DefaultSchemes.LazyFetch(u)
   132  }
   133  
   134  // LazyFetch returns a reader that will Fetch the file given by `u` when
   135  // Read is called, based on `u`s scheme. See Schemes.Fetch for more
   136  // details.
   137  func (s Schemes) LazyFetch(u *url.URL) (io.ReaderAt, error) {
   138  	fg, ok := s[u.Scheme]
   139  	if !ok {
   140  		return nil, &URLError{URL: u, Err: ErrNoSuchScheme}
   141  	}
   142  
   143  	return &file{
   144  		url: u,
   145  		ReaderAt: uio.NewLazyOpenerAt(u.String(), func() (io.ReaderAt, error) {
   146  			r, err := fg.Fetch(u)
   147  			if err != nil {
   148  				return nil, &URLError{URL: u, Err: err}
   149  			}
   150  			return r, nil
   151  		}),
   152  	}, nil
   153  }
   154  
   155  // TFTPClient implements FileScheme for TFTP files.
   156  type TFTPClient struct {
   157  	opts []tftp.ClientOpt
   158  }
   159  
   160  // NewTFTPClient returns a new TFTP client based on the given tftp.ClientOpt.
   161  func NewTFTPClient(opts ...tftp.ClientOpt) FileScheme {
   162  	return &TFTPClient{
   163  		opts: opts,
   164  	}
   165  }
   166  
   167  // Fetch implements FileScheme.Fetch.
   168  func (t *TFTPClient) Fetch(u *url.URL) (io.ReaderAt, error) {
   169  	// TODO(hugelgupf): These clients are basically stateless, except for
   170  	// the options. Figure out whether you actually have to re-establish
   171  	// this connection every time. Audit the TFTP library.
   172  	c, err := tftp.NewClient(t.opts...)
   173  	if err != nil {
   174  		return nil, err
   175  	}
   176  
   177  	r, err := c.Get(u.String())
   178  	if err != nil {
   179  		return nil, err
   180  	}
   181  	return uio.NewCachingReader(r), nil
   182  }
   183  
   184  // SchemeWithRetries wraps a FileScheme and automatically retries (with
   185  // backoff) when Fetch returns a non-nil err.
   186  type SchemeWithRetries struct {
   187  	Scheme  FileScheme
   188  	BackOff backoff.BackOff
   189  }
   190  
   191  // Fetch implements FileScheme.Fetch.
   192  func (s *SchemeWithRetries) Fetch(u *url.URL) (io.ReaderAt, error) {
   193  	var err error
   194  	s.BackOff.Reset()
   195  	for d := time.Duration(0); d != backoff.Stop; d = s.BackOff.NextBackOff() {
   196  		if d > 0 {
   197  			time.Sleep(d)
   198  		}
   199  
   200  		var r io.ReaderAt
   201  		r, err = s.Scheme.Fetch(u)
   202  		if err != nil {
   203  			log.Printf("Error: Getting %v: %v", u, err)
   204  			continue
   205  		}
   206  		return r, nil
   207  	}
   208  
   209  	log.Printf("Error: Too many retries to get file %v", u)
   210  	return nil, err
   211  }
   212  
   213  // HTTPClient implements FileScheme for HTTP files.
   214  type HTTPClient struct {
   215  	c *http.Client
   216  }
   217  
   218  // NewHTTPClient returns a new HTTP FileScheme based on the given http.Client.
   219  func NewHTTPClient(c *http.Client) *HTTPClient {
   220  	return &HTTPClient{
   221  		c: c,
   222  	}
   223  }
   224  
   225  // Fetch implements FileScheme.Fetch.
   226  func (h HTTPClient) Fetch(u *url.URL) (io.ReaderAt, error) {
   227  	resp, err := h.c.Get(u.String())
   228  	if err != nil {
   229  		return nil, err
   230  	}
   231  
   232  	if resp.StatusCode != 200 {
   233  		return nil, fmt.Errorf("HTTP server responded with code %d, want 200: response %v", resp.StatusCode, resp)
   234  	}
   235  	return uio.NewCachingReader(resp.Body), nil
   236  }
   237  
   238  // HTTPClientWithRetries implements FileScheme for HTTP files and automatically
   239  // retries (with backoff) upon an error.
   240  type HTTPClientWithRetries struct {
   241  	Client  *http.Client
   242  	BackOff backoff.BackOff
   243  }
   244  
   245  // Fetch implements FileScheme.Fetch.
   246  func (h HTTPClientWithRetries) Fetch(u *url.URL) (io.ReaderAt, error) {
   247  	req, err := http.NewRequest("GET", u.String(), nil)
   248  	if err != nil {
   249  		return nil, err
   250  	}
   251  
   252  	h.BackOff.Reset()
   253  	for d := time.Duration(0); d != backoff.Stop; d = h.BackOff.NextBackOff() {
   254  		if d > 0 {
   255  			time.Sleep(d)
   256  		}
   257  
   258  		var resp *http.Response
   259  		// Note: err uses the scope outside the for loop.
   260  		resp, err = h.Client.Do(req)
   261  		if err != nil {
   262  			log.Printf("Error: HTTP client: %v", err)
   263  			continue
   264  		}
   265  		if resp.StatusCode != 200 {
   266  			log.Printf("Error: HTTP server responded with code %d, want 200: response %v", resp.StatusCode, resp)
   267  			continue
   268  		}
   269  		return uio.NewCachingReader(resp.Body), nil
   270  	}
   271  	log.Printf("Error: Too many retries to download %v", u)
   272  	return nil, fmt.Errorf("too many HTTP retries: %v", err)
   273  }
   274  
   275  // LocalFileClient implements FileScheme for files on disk.
   276  type LocalFileClient struct{}
   277  
   278  // Fetch implements FileScheme.Fetch.
   279  func (lfs LocalFileClient) Fetch(u *url.URL) (io.ReaderAt, error) {
   280  	return os.Open(filepath.Clean(u.Path))
   281  }