github.com/quay/claircore@v1.5.28/pkg/ovalutil/fetcher.go (about) 1 package ovalutil 2 3 import ( 4 "compress/bzip2" 5 "context" 6 "encoding/json" 7 "fmt" 8 "io" 9 "mime" 10 "net/http" 11 "net/url" 12 "path" 13 14 "github.com/quay/zlog" 15 16 "github.com/quay/claircore/libvuln/driver" 17 "github.com/quay/claircore/pkg/tmp" 18 ) 19 20 // Compressor is used by Fetcher to decompress data it fetches. 21 type Compressor uint 22 23 //go:generate -command stringer go run golang.org/x/tools/cmd/stringer 24 //go:generate stringer -type Compressor -linecomment 25 26 // These are the kinds of Compession a Fetcher can deal with. 27 const ( 28 CompressionAuto Compressor = iota // auto 29 CompressionNone // none 30 CompressionGzip // gzip 31 CompressionBzip2 // bzip2 32 CompressionZstd // zstd 33 ) 34 35 // ParseCompressor reports the Compressor indicated by the passed in string. 36 func ParseCompressor(s string) (c Compressor, err error) { 37 switch s { 38 case "gz", "gzip": 39 c = CompressionGzip 40 case "bz2", "bzip2": 41 c = CompressionBzip2 42 case "zstd": 43 c = CompressionZstd 44 case "none": 45 c = CompressionNone 46 case "", "auto": 47 c = CompressionAuto 48 default: 49 return c, fmt.Errorf("ovalutil: unknown compression scheme %q", s) 50 } 51 return c, nil 52 } 53 54 // Fetcher implements the driver.Fetcher interface. 55 // 56 // Fetcher expects all of its exported members to be filled out appropriately, 57 // and may panic if not. 58 type Fetcher struct { 59 URL *url.URL 60 Client *http.Client 61 Compression Compressor 62 } 63 64 // Configure implements driver.Configurable. 65 // 66 // For users that embed a Fetcher, this provides a configuration hook by 67 // default. 68 func (f *Fetcher) Configure(ctx context.Context, cf driver.ConfigUnmarshaler, c *http.Client) error { 69 ctx = zlog.ContextWithValues(ctx, "component", "pkg/ovalutil/Fetcher.Configure") 70 var cfg FetcherConfig 71 if err := cf(&cfg); err != nil { 72 return err 73 } 74 if cfg.URL != "" { 75 uri, err := url.Parse(cfg.URL) 76 if err != nil { 77 return err 78 } 79 f.URL = uri 80 zlog.Info(ctx). 81 Msg("configured database URL") 82 } 83 if cfg.Compression != "" { 84 c, err := ParseCompressor(cfg.Compression) 85 if err != nil { 86 return err 87 } 88 f.Compression = c 89 zlog.Info(ctx). 90 Msg("configured database compression") 91 } 92 93 f.Client = c 94 return nil 95 } 96 97 // FetcherConfig is the configuration that the Fetcher's Configure method works 98 // with. 99 // 100 // Users the embed Fetcher and use Fetcher.Configure should make sure any of 101 // their configuration keys don't conflict with these names. 102 type FetcherConfig struct { 103 URL string `json:"url" yaml:"url"` 104 Compression string `json:"compression" yaml:"compression"` 105 } 106 107 // Fetch fetches the resource as specified by Fetcher.URL and 108 // Fetcher.Compression, using the client provided as Fetcher.Client. 109 // 110 // Fetch makes GET requests, and will make conditional requests using the 111 // passed-in hint. 112 // 113 // Tmp.File is used to return a ReadCloser that outlives the passed-in context. 114 func (f *Fetcher) Fetch(ctx context.Context, hint driver.Fingerprint) (io.ReadCloser, driver.Fingerprint, error) { 115 ctx = zlog.ContextWithValues(ctx, "component", "pkg/ovalutil/Fetcher.Fetch") 116 zlog.Info(ctx).Str("database", f.URL.String()).Msg("starting fetch") 117 req := http.Request{ 118 Method: http.MethodGet, 119 Header: http.Header{ 120 "User-Agent": {"claircore/pkg/ovalutil.Fetcher"}, 121 }, 122 URL: f.URL, 123 Proto: "HTTP/1.1", 124 ProtoMajor: 1, 125 ProtoMinor: 1, 126 Host: f.URL.Host, 127 } 128 var fp fingerprint 129 if h := string(hint); h != "" { 130 if err := json.Unmarshal([]byte(h), &fp); err == nil { 131 fp.Set(req.Header) 132 } 133 } 134 135 res, err := f.Client.Do(req.WithContext(ctx)) 136 if res != nil { 137 defer res.Body.Close() 138 } 139 if err != nil { 140 return nil, hint, err 141 } 142 switch res.StatusCode { 143 case http.StatusOK: 144 if fp.Etag == "" || fp.Etag != res.Header.Get("etag") { 145 break 146 } 147 fallthrough 148 case http.StatusNotModified: 149 return nil, hint, driver.Unchanged 150 default: 151 return nil, hint, fmt.Errorf("ovalutil: fetcher got unexpected HTTP response: %d (%s)", res.StatusCode, res.Status) 152 } 153 zlog.Debug(ctx).Msg("request ok") 154 155 var r io.Reader 156 cmp := f.Compression 157 Compression: 158 switch cmp { 159 case CompressionAuto: 160 var kind string 161 for _, h := range []string{`content-type`, `content-disposition`} { 162 v := res.Header.Get(h) 163 if v == "" { 164 continue 165 } 166 switch h { 167 case `content-type`: 168 kind, _, err = mime.ParseMediaType(v) 169 if err == nil { 170 goto Found 171 } 172 case `content-disposition`: 173 var params map[string]string 174 _, params, err = mime.ParseMediaType(v) 175 if err != nil { 176 break 177 } 178 fn, ok := params["filename"] 179 if !ok { 180 break 181 } 182 kind = mime.TypeByExtension(path.Ext(fn)) 183 if kind != "" { 184 goto Found 185 } 186 default: 187 panic("unreachable") 188 } 189 zlog.Debug(ctx). 190 Err(err). 191 Str("header", h). 192 Str("value", v). 193 Msg("failed to parse incoming HTTP header") 194 } 195 kind = mime.TypeByExtension(path.Ext(res.Request.URL.Path)) 196 Found: 197 switch kind { 198 case `application/x-bzip2`: 199 cmp = CompressionBzip2 200 case `application/gzip`, `application/x-gzip`: 201 cmp = CompressionGzip 202 case `application/zstd`: 203 cmp = CompressionZstd 204 default: 205 // unknown type 206 cmp = CompressionNone 207 } 208 goto Compression 209 case CompressionNone: 210 r = res.Body 211 case CompressionGzip: 212 gz, err := getGzip(res.Body) 213 if err != nil { 214 return nil, hint, err 215 } 216 defer putGzip(gz) 217 r = gz 218 case CompressionBzip2: 219 r = bzip2.NewReader(res.Body) 220 case CompressionZstd: 221 zz, err := getZstd(res.Body) 222 if err != nil { 223 return nil, hint, err 224 } 225 defer putZstd(zz) 226 r = zz 227 default: 228 panic(fmt.Sprintf("ovalutil: programmer error: unknown compression scheme: %v", f.Compression)) 229 } 230 zlog.Debug(ctx). 231 Stringer("compression", cmp). 232 Msg("found compression scheme") 233 234 tf, err := tmp.NewFile("", "fetcher.") 235 if err != nil { 236 return nil, hint, err 237 } 238 zlog.Debug(ctx). 239 Str("path", tf.Name()). 240 Msg("using tempfile") 241 success := false 242 defer func() { 243 if !success { 244 zlog.Debug(ctx).Msg("unsuccessful, cleaning up tempfile") 245 if err := tf.Close(); err != nil { 246 zlog.Warn(ctx).Err(err).Msg("failed to close tempfile") 247 } 248 } 249 }() 250 251 if _, err := io.Copy(tf, r); err != nil { 252 return nil, hint, err 253 } 254 if o, err := tf.Seek(0, io.SeekStart); err != nil || o != 0 { 255 return nil, hint, err 256 } 257 zlog.Debug(ctx).Msg("decompressed and buffered database") 258 259 fp.From(res.Header) 260 hint = fp.Fingerprint() 261 success = true 262 return tf, hint, nil 263 } 264 265 type fingerprint struct { 266 Etag string `json:",omitempty"` 267 Date string `json:",omitempty"` 268 } 269 270 func (f fingerprint) Set(h http.Header) { 271 if f.Etag != "" { 272 h.Set("if-none-match", f.Etag) 273 } 274 if f.Date != "" { 275 h.Set("if-modified-since", f.Date) 276 } 277 } 278 279 func (f *fingerprint) From(h http.Header) { 280 if tag := h.Get("etag"); tag != "" { 281 f.Etag = tag 282 } 283 f.Date = h.Get("last-modified") 284 } 285 286 func (f fingerprint) Fingerprint() driver.Fingerprint { 287 b, _ := json.Marshal(f) 288 return driver.Fingerprint(string(b)) 289 }