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 }