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  }