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 }