github.com/ratrocket/u-root@v0.0.0-20180201221235-1cf9f48ee2cf/pkg/pxe/schemes.go (about)

     1  package pxe
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"io"
     7  	"net/http"
     8  	"net/url"
     9  	"os"
    10  	"path/filepath"
    11  
    12  	"pack.ag/tftp"
    13  )
    14  
    15  var (
    16  	// ErrNoSuchScheme is returned by Schemes.GetFile and
    17  	// Schemes.LazyGetFile if there is no registered FileScheme
    18  	// implementation for the given URI scheme.
    19  	ErrNoSuchScheme = errors.New("no such scheme")
    20  )
    21  
    22  // FileScheme represents the implementation of a URI scheme and gives access to
    23  // downloading files of that scheme.
    24  //
    25  // For example, an http FileScheme implementation would download files using
    26  // the HTTP protocol.
    27  type FileScheme interface {
    28  	// GetFile returns a reader that gives the contents of `u`.
    29  	//
    30  	// It may do so by downloading `u` and placing it in a buffer, or by
    31  	// returning an io.Reader that downloads the file.
    32  	GetFile(u *url.URL) (io.Reader, error)
    33  }
    34  
    35  var (
    36  	// DefaultHTTPClient is the default HTTP FileScheme.
    37  	//
    38  	// It is not recommended to use this for HTTPS. We recommend creating an
    39  	// http.Client that accepts only a private pool of certificates.
    40  	DefaultHTTPClient = NewHTTPClient(http.DefaultClient)
    41  
    42  	// DefaultTFTPClient is the default TFTP FileScheme.
    43  	DefaultTFTPClient FileScheme
    44  
    45  	// DefaultSchemes are the schemes supported by PXE by default.
    46  	DefaultSchemes Schemes
    47  )
    48  
    49  func init() {
    50  	c, err := tftp.NewClient()
    51  	if err != nil {
    52  		panic(fmt.Sprintf("tftp.NewClient failed: %v", err))
    53  	}
    54  	DefaultTFTPClient = NewTFTPClient(c)
    55  
    56  	DefaultSchemes = Schemes{
    57  		"tftp": NewCachedFileScheme(DefaultTFTPClient),
    58  		"http": NewCachedFileScheme(DefaultHTTPClient),
    59  		"file": NewCachedFileScheme(&LocalFileClient{}),
    60  	}
    61  }
    62  
    63  // Schemes is a map of URI scheme identifier -> implementation that can
    64  // download a file for that scheme.
    65  type Schemes map[string]FileScheme
    66  
    67  // RegisterScheme calls DefaultSchemes.Register.
    68  func RegisterScheme(scheme string, fs FileScheme) {
    69  	DefaultSchemes.Register(scheme, fs)
    70  }
    71  
    72  // Register registers a scheme identified by `scheme` to be `fs`.
    73  func (s Schemes) Register(scheme string, fs FileScheme) {
    74  	s[scheme] = fs
    75  }
    76  
    77  func parseURI(uri string, wd *url.URL) (*url.URL, error) {
    78  	u, err := url.Parse(uri)
    79  	if err != nil {
    80  		return nil, err
    81  	}
    82  
    83  	if len(u.Scheme) == 0 {
    84  		u.Scheme = wd.Scheme
    85  
    86  		if len(u.Host) == 0 {
    87  			// If this is not there, it was likely just a path.
    88  			u.Host = wd.Host
    89  			u.Path = filepath.Join(wd.Path, filepath.Clean(u.Path))
    90  		}
    91  	}
    92  	return u, nil
    93  }
    94  
    95  // GetFile downloads a file via DefaultSchemes. See Schemes.GetFile for
    96  // details.
    97  func GetFile(uri string, wd *url.URL) (io.Reader, error) {
    98  	return DefaultSchemes.GetFile(uri, wd)
    99  }
   100  
   101  // GetFile downloads the file with the given `uri`. `uri.Scheme` is used to
   102  // select the FileScheme via `s`.
   103  //
   104  // If `s` does not contain a FileScheme for `uri.Scheme`, ErrNoSuchScheme is
   105  // returned.
   106  //
   107  // If `uri` is just a relative path and not a full URI, `wd` is used as the
   108  // "working directory" of that relative path; the resulting URI is roughly
   109  // `path.Join(wd.String(), uri)`.
   110  func (s Schemes) GetFile(uri string, wd *url.URL) (io.Reader, error) {
   111  	u, err := parseURI(uri, wd)
   112  	if err != nil {
   113  		return nil, err
   114  	}
   115  
   116  	fg, ok := s[u.Scheme]
   117  	if !ok {
   118  		return nil, ErrNoSuchScheme
   119  	}
   120  	return fg.GetFile(u)
   121  }
   122  
   123  // LazyGetFile calls LazyGetFile on DefaultSchemes. See Schemes.LazyGetFile.
   124  func LazyGetFile(uri string, wd *url.URL) (io.Reader, error) {
   125  	return DefaultSchemes.LazyGetFile(uri, wd)
   126  }
   127  
   128  // LazyGetFile returns a reader that will download the file given by `uri` when
   129  // Read is called, based on `uri`s scheme. See Schemes.GetFile for more
   130  // details.
   131  func (s Schemes) LazyGetFile(uri string, wd *url.URL) (io.Reader, error) {
   132  	u, err := parseURI(uri, wd)
   133  	if err != nil {
   134  		return nil, err
   135  	}
   136  
   137  	fg, ok := s[u.Scheme]
   138  	if !ok {
   139  		return nil, fmt.Errorf("could not get file based on scheme %q: no such scheme registered", u.Scheme)
   140  	}
   141  
   142  	return NewLazyOpener(func() (io.Reader, error) {
   143  		return fg.GetFile(u)
   144  	}), nil
   145  }
   146  
   147  // TFTPClient implements FileScheme for TFTP files.
   148  type TFTPClient struct {
   149  	c *tftp.Client
   150  }
   151  
   152  // NewTFTPClient returns a new TFTP client based on the given tftp.Client.
   153  func NewTFTPClient(c *tftp.Client) FileScheme {
   154  	return &TFTPClient{
   155  		c: c,
   156  	}
   157  }
   158  
   159  // GetFile implements FileScheme.GetFile.
   160  func (t *TFTPClient) GetFile(u *url.URL) (io.Reader, error) {
   161  	r, err := t.c.Get(u.String())
   162  	if err != nil {
   163  		return nil, fmt.Errorf("failed to get %q: %v", u, err)
   164  	}
   165  	return r, nil
   166  }
   167  
   168  // HTTPClient implements FileScheme for HTTP files.
   169  type HTTPClient struct {
   170  	c *http.Client
   171  }
   172  
   173  // NewHTTPClient returns a new HTTP FileScheme based on the given http.Client.
   174  func NewHTTPClient(c *http.Client) *HTTPClient {
   175  	return &HTTPClient{
   176  		c: c,
   177  	}
   178  }
   179  
   180  // GetFile implements FileScheme.GetFile.
   181  func (h HTTPClient) GetFile(u *url.URL) (io.Reader, error) {
   182  	resp, err := h.c.Get(u.String())
   183  	if err != nil {
   184  		return nil, fmt.Errorf("Could not download file %s: %v", u, err)
   185  	}
   186  	if resp.StatusCode != 200 {
   187  		return nil, fmt.Errorf("could not download file %s: response %v", u, resp)
   188  	}
   189  	return resp.Body, nil
   190  }
   191  
   192  // LocalFileClient implements FileScheme for files on disk.
   193  type LocalFileClient struct{}
   194  
   195  // GetFile implements FileScheme.GetFile.
   196  func (lfs LocalFileClient) GetFile(u *url.URL) (io.Reader, error) {
   197  	return os.Open(filepath.Clean(u.Path))
   198  }
   199  
   200  type cachedFile struct {
   201  	cr  *CachingReader
   202  	err error
   203  }
   204  
   205  // CachedFileScheme implements FileScheme and caches files downloaded from a
   206  // FileScheme.
   207  type CachedFileScheme struct {
   208  	fs FileScheme
   209  
   210  	// cache is a map of URI string -> cached file or error object.
   211  	cache map[string]cachedFile
   212  }
   213  
   214  // NewCachedFileScheme returns a caching wrapper for the given FileScheme `fs`.
   215  func NewCachedFileScheme(fs FileScheme) FileScheme {
   216  	return &CachedFileScheme{
   217  		fs:    fs,
   218  		cache: make(map[string]cachedFile),
   219  	}
   220  }
   221  
   222  // GetFile implements FileScheme.GetFile.
   223  func (cc *CachedFileScheme) GetFile(u *url.URL) (io.Reader, error) {
   224  	uri := u.String()
   225  	if cf, ok := cc.cache[uri]; ok {
   226  		if cf.err != nil {
   227  			return nil, cf.err
   228  		}
   229  		return cf.cr.NewReader(), nil
   230  	}
   231  
   232  	r, err := cc.fs.GetFile(u)
   233  	if err != nil {
   234  		cc.cache[uri] = cachedFile{err: err}
   235  		return nil, err
   236  	}
   237  	cr := NewCachingReader(r)
   238  	cc.cache[uri] = cachedFile{cr: cr}
   239  	return cr.NewReader(), nil
   240  }