github.com/andrewsun2898/u-root@v6.0.1-0.20200616011413-4b2895c1b815+incompatible/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 "context" 12 "errors" 13 "fmt" 14 "io" 15 "log" 16 "net/http" 17 "net/url" 18 "os" 19 "path/filepath" 20 "strings" 21 "time" 22 23 "github.com/cenkalti/backoff" 24 "github.com/u-root/u-root/pkg/uio" 25 "pack.ag/tftp" 26 ) 27 28 var ( 29 // ErrNoSuchScheme is returned by Schemes.Fetch and 30 // Schemes.LazyFetch if there is no registered FileScheme 31 // implementation for the given URL scheme. 32 ErrNoSuchScheme = errors.New("no such scheme") 33 ) 34 35 // File is a reference to a file fetched through this library. 36 type File interface { 37 io.ReaderAt 38 39 // URL is the file's original URL. 40 URL() *url.URL 41 } 42 43 // FileScheme represents the implementation of a URL scheme and gives access to 44 // fetching files of that scheme. 45 // 46 // For example, an http FileScheme implementation would fetch files using 47 // the HTTP protocol. 48 type FileScheme interface { 49 // Fetch returns a reader that gives the contents of `u`. 50 // 51 // It may do so by fetching `u` and placing it in a buffer, or by 52 // returning an io.ReaderAt that fetchs the file. 53 Fetch(ctx context.Context, u *url.URL) (io.ReaderAt, error) 54 } 55 56 // FileSchemeRetryFilter contains extra RetryFilter method for a FileScheme 57 // wrapped by SchemeWithRetries. 58 type FileSchemeRetryFilter interface { 59 // RetryFilter lets a FileScheme filter for errors returned by Fetch 60 // which are worth retrying. If this interface is not implemented, the 61 // default for SchemeWithRetries is to always retry. RetryFilter 62 // returns true to indicate a request should be retried. 63 RetryFilter(u *url.URL, err error) bool 64 } 65 66 var ( 67 // DefaultHTTPClient is the default HTTP FileScheme. 68 // 69 // It is not recommended to use this for HTTPS. We recommend creating an 70 // http.Client that accepts only a private pool of certificates. 71 DefaultHTTPClient = NewHTTPClient(http.DefaultClient) 72 73 // DefaultTFTPClient is the default TFTP FileScheme. 74 DefaultTFTPClient = NewTFTPClient(tftp.ClientMode(tftp.ModeOctet), tftp.ClientBlocksize(1450), tftp.ClientWindowsize(65535)) 75 76 // DefaultSchemes are the schemes supported by default. 77 DefaultSchemes = Schemes{ 78 "tftp": DefaultTFTPClient, 79 "http": DefaultHTTPClient, 80 "file": &LocalFileClient{}, 81 } 82 ) 83 84 // URLError is an error involving URLs. 85 type URLError struct { 86 URL *url.URL 87 Err error 88 } 89 90 // Error implements error.Error. 91 func (s *URLError) Error() string { 92 return fmt.Sprintf("encountered error %v with %q", s.Err, s.URL) 93 } 94 95 // IsURLError returns true iff err is a URLError. 96 func IsURLError(err error) bool { 97 _, ok := err.(*URLError) 98 return ok 99 } 100 101 // Schemes is a map of URL scheme identifier -> implementation that can 102 // fetch a file for that scheme. 103 type Schemes map[string]FileScheme 104 105 // RegisterScheme calls DefaultSchemes.Register. 106 func RegisterScheme(scheme string, fs FileScheme) { 107 DefaultSchemes.Register(scheme, fs) 108 } 109 110 // Register registers a scheme identified by `scheme` to be `fs`. 111 func (s Schemes) Register(scheme string, fs FileScheme) { 112 s[scheme] = fs 113 } 114 115 // Fetch fetchs a file via DefaultSchemes. 116 func Fetch(ctx context.Context, u *url.URL) (File, error) { 117 return DefaultSchemes.Fetch(ctx, u) 118 } 119 120 // file is an io.ReaderAt with a nice Stringer. 121 type file struct { 122 io.ReaderAt 123 124 url *url.URL 125 } 126 127 // URL returns the file URL. 128 func (f file) URL() *url.URL { 129 return f.url 130 } 131 132 // String implements fmt.Stringer. 133 func (f file) String() string { 134 return f.url.String() 135 } 136 137 // Fetch fetchs the file with the given `u`. `u.Scheme` is used to 138 // select the FileScheme via `s`. 139 // 140 // If `s` does not contain a FileScheme for `u.Scheme`, ErrNoSuchScheme is 141 // returned. 142 func (s Schemes) Fetch(ctx context.Context, u *url.URL) (File, error) { 143 fg, ok := s[u.Scheme] 144 if !ok { 145 return nil, &URLError{URL: u, Err: ErrNoSuchScheme} 146 } 147 r, err := fg.Fetch(ctx, u) 148 if err != nil { 149 return nil, &URLError{URL: u, Err: err} 150 } 151 return &file{ReaderAt: r, url: u}, nil 152 } 153 154 // RetryFilter implements FileSchemeRetryFilter. 155 func (s Schemes) RetryFilter(u *url.URL, err error) bool { 156 fg, ok := s[u.Scheme] 157 if !ok { 158 return false 159 } 160 if fg, ok := fg.(FileSchemeRetryFilter); ok { 161 return fg.RetryFilter(u, err) 162 } 163 return true 164 } 165 166 // LazyFetch calls LazyFetch on DefaultSchemes. 167 func LazyFetch(u *url.URL) (File, error) { 168 return DefaultSchemes.LazyFetch(u) 169 } 170 171 // LazyFetch returns a reader that will Fetch the file given by `u` when 172 // Read is called, based on `u`s scheme. See Schemes.Fetch for more 173 // details. 174 func (s Schemes) LazyFetch(u *url.URL) (File, error) { 175 fg, ok := s[u.Scheme] 176 if !ok { 177 return nil, &URLError{URL: u, Err: ErrNoSuchScheme} 178 } 179 180 return &file{ 181 url: u, 182 ReaderAt: uio.NewLazyOpenerAt(u.String(), func() (io.ReaderAt, error) { 183 // TODO 184 r, err := fg.Fetch(context.TODO(), u) 185 if err != nil { 186 return nil, &URLError{URL: u, Err: err} 187 } 188 return r, nil 189 }), 190 }, nil 191 } 192 193 // TFTPClient implements FileScheme for TFTP files. 194 type TFTPClient struct { 195 opts []tftp.ClientOpt 196 } 197 198 // NewTFTPClient returns a new TFTP client based on the given tftp.ClientOpt. 199 func NewTFTPClient(opts ...tftp.ClientOpt) FileScheme { 200 return &TFTPClient{ 201 opts: opts, 202 } 203 } 204 205 // Fetch implements FileScheme.Fetch. 206 func (t *TFTPClient) Fetch(_ context.Context, u *url.URL) (io.ReaderAt, error) { 207 // TODO(hugelgupf): These clients are basically stateless, except for 208 // the options. Figure out whether you actually have to re-establish 209 // this connection every time. Audit the TFTP library. 210 c, err := tftp.NewClient(t.opts...) 211 if err != nil { 212 return nil, err 213 } 214 215 r, err := c.Get(u.String()) 216 if err != nil { 217 return nil, err 218 } 219 return uio.NewCachingReader(r), nil 220 } 221 222 // RetryFilter implements FileSchemeRetryFilter. 223 func (t *TFTPClient) RetryFilter(u *url.URL, err error) bool { 224 // The tftp does not export the necessary structs to get the 225 // code out of the error message cleanly. 226 return !strings.Contains(err.Error(), "FILE_NOT_FOUND") 227 } 228 229 // SchemeWithRetries wraps a FileScheme and automatically retries (with 230 // backoff) when Fetch returns a non-nil err. 231 type SchemeWithRetries struct { 232 Scheme FileScheme 233 BackOff backoff.BackOff 234 } 235 236 // Fetch implements FileScheme.Fetch. 237 func (s *SchemeWithRetries) Fetch(ctx context.Context, u *url.URL) (io.ReaderAt, error) { 238 var err error 239 s.BackOff.Reset() 240 for d := time.Duration(0); d != backoff.Stop; d = s.BackOff.NextBackOff() { 241 if d > 0 { 242 time.Sleep(d) 243 } 244 245 var r io.ReaderAt 246 // Note: err uses the scope outside the for loop. 247 r, err = s.Scheme.Fetch(ctx, u) 248 if err == nil { 249 return r, nil 250 } 251 252 log.Printf("Error: Getting %v: %v", u, err) 253 if s, ok := s.Scheme.(FileSchemeRetryFilter); ok && !s.RetryFilter(u, err) { 254 return r, err 255 } 256 log.Printf("Retrying %v", u) 257 } 258 259 log.Printf("Error: Too many retries to get file %v", u) 260 return nil, err 261 } 262 263 // HTTPClientCodeError is returned by HTTPClient.Fetch when the server replies 264 // with a non-200 code. 265 type HTTPClientCodeError struct { 266 Err error 267 HTTPCode int 268 } 269 270 // Error implements error for HTTPClientCodeError. 271 func (h *HTTPClientCodeError) Error() string { 272 return fmt.Sprintf("HTTP server responded with error code %d, want 200: response %v", h.HTTPCode, h.Err) 273 } 274 275 // HTTPClient implements FileScheme for HTTP files. 276 type HTTPClient struct { 277 c *http.Client 278 } 279 280 // NewHTTPClient returns a new HTTP FileScheme based on the given http.Client. 281 func NewHTTPClient(c *http.Client) *HTTPClient { 282 return &HTTPClient{ 283 c: c, 284 } 285 } 286 287 // Fetch implements FileScheme.Fetch. 288 func (h HTTPClient) Fetch(ctx context.Context, u *url.URL) (io.ReaderAt, error) { 289 req, err := http.NewRequestWithContext(ctx, "GET", u.String(), nil) 290 if err != nil { 291 return nil, err 292 } 293 resp, err := h.c.Do(req) 294 if err != nil { 295 return nil, err 296 } 297 298 if resp.StatusCode != 200 { 299 return nil, &HTTPClientCodeError{err, resp.StatusCode} 300 } 301 return uio.NewCachingReader(resp.Body), nil 302 } 303 304 // RetryFilter implements FileSchemeRetryFilter. 305 func (h HTTPClient) RetryFilter(u *url.URL, err error) bool { 306 e, ok := err.(*HTTPClientCodeError) 307 if !ok { 308 return true 309 } 310 switch c := e.HTTPCode; { 311 case c == 200: 312 return false 313 case c == 408, c == 409, c == 425, c == 429: 314 // Retry for codes "Request Timeout(408), Conflict(409), Too Early(425), and Too Many Requests(429)" 315 return true 316 case c >= 400 && c < 500: 317 // We don't retry all other 400 codes, since the situation won't be improved with a retry. 318 return false 319 default: 320 return true 321 } 322 } 323 324 // LocalFileClient implements FileScheme for files on disk. 325 type LocalFileClient struct{} 326 327 // Fetch implements FileScheme.Fetch. 328 func (lfs LocalFileClient) Fetch(_ context.Context, u *url.URL) (io.ReaderAt, error) { 329 return os.Open(filepath.Clean(u.Path)) 330 }