github.com/quay/claircore@v1.5.28/aws/client.go (about) 1 package aws 2 3 import ( 4 "compress/gzip" 5 "context" 6 "encoding/xml" 7 "errors" 8 "fmt" 9 "io" 10 "net/http" 11 "net/url" 12 "path" 13 "strings" 14 "time" 15 16 "github.com/quay/zlog" 17 18 "github.com/quay/claircore/aws/internal/alas" 19 "github.com/quay/claircore/internal/xmlutil" 20 "github.com/quay/claircore/pkg/tmp" 21 ) 22 23 const ( 24 repoDataPath = "/repodata/repomd.xml" 25 updatesPath = "/repodata/updateinfo.xml.gz" 26 defaultOpTimeout = 15 * time.Second 27 ) 28 29 // Client is an http for accessing ALAS mirrors. 30 type Client struct { 31 c *http.Client 32 mirrors []*url.URL 33 } 34 35 func NewClient(ctx context.Context, hc *http.Client, release Release) (*Client, error) { 36 ctx = zlog.ContextWithValues(ctx, "release", string(release)) 37 if hc == nil { 38 return nil, errors.New("http.Client not provided") 39 } 40 client := &Client{ 41 c: hc, 42 mirrors: []*url.URL{}, 43 } 44 tctx, cancel := context.WithTimeout(ctx, defaultOpTimeout) 45 defer cancel() 46 err := client.getMirrors(tctx, release.mirrorlist()) 47 return client, err 48 } 49 50 // RepoMD returns a alas.RepoMD containing sha256 information of a repositories contents 51 func (c *Client) RepoMD(ctx context.Context) (alas.RepoMD, error) { 52 ctx = zlog.ContextWithValues(ctx, "component", "aws/Client.RepoMD") 53 for _, mirror := range c.mirrors { 54 m := *mirror 55 m.Path = path.Join(m.Path, repoDataPath) 56 ctx := zlog.ContextWithValues(ctx, "mirror", m.String()) 57 58 req, err := http.NewRequestWithContext(ctx, http.MethodGet, m.String(), nil) 59 if err != nil { 60 zlog.Error(ctx).Err(err).Msg("failed to make request object") 61 continue 62 } 63 64 zlog.Debug(ctx).Msg("attempting repomd download") 65 resp, err := c.c.Do(req) 66 if err != nil { 67 zlog.Error(ctx).Err(err).Msg("failed to retrieve repomd") 68 continue 69 } 70 defer resp.Body.Close() 71 72 switch resp.StatusCode { 73 case http.StatusOK: 74 // break 75 default: 76 zlog.Error(ctx). 77 Int("code", resp.StatusCode). 78 Str("status", resp.Status). 79 Msg("unexpected HTTP response") 80 continue 81 } 82 83 repoMD := alas.RepoMD{} 84 dec := xml.NewDecoder(resp.Body) 85 dec.CharsetReader = xmlutil.CharsetReader 86 if err := dec.Decode(&repoMD); err != nil { 87 zlog.Error(ctx). 88 Err(err). 89 Msg("failed xml unmarshal") 90 continue 91 } 92 93 zlog.Debug(ctx).Msg("success") 94 return repoMD, nil 95 } 96 97 zlog.Error(ctx).Msg("exhausted all mirrors") 98 return alas.RepoMD{}, fmt.Errorf("all mirrors failed to retrieve repo metadata") 99 } 100 101 // Updates returns the *http.Response of the first mirror to establish a connection 102 func (c *Client) Updates(ctx context.Context) (io.ReadCloser, error) { 103 ctx = zlog.ContextWithValues(ctx, "component", "aws/Client.Updates") 104 for _, mirror := range c.mirrors { 105 m := *mirror 106 m.Path = path.Join(m.Path, updatesPath) 107 ctx := zlog.ContextWithValues(ctx, "mirror", m.String()) 108 109 req, err := http.NewRequestWithContext(ctx, http.MethodGet, m.String(), nil) 110 if err != nil { 111 zlog.Error(ctx).Err(err).Msg("failed to make request object") 112 continue 113 } 114 115 tf, err := tmp.NewFile("", "") 116 if err != nil { 117 zlog.Error(ctx).Err(err).Msg("failed to open temp file") 118 continue 119 } 120 var success bool 121 defer func() { 122 if !success { 123 if err := tf.Close(); err != nil { 124 zlog.Warn(ctx).Err(err).Msg("unable to close spool") 125 } 126 } 127 }() 128 129 resp, err := c.c.Do(req) 130 if err != nil { 131 zlog.Error(ctx).Err(err).Msg("failed to retrieve updates") 132 continue 133 } 134 defer resp.Body.Close() 135 136 switch resp.StatusCode { 137 case http.StatusOK: 138 // break 139 default: 140 zlog.Error(ctx). 141 Int("code", resp.StatusCode). 142 Str("status", resp.Status). 143 Msg("unexpected HTTP response") 144 continue 145 } 146 147 if _, err := io.Copy(tf, resp.Body); err != nil { 148 return nil, err 149 } 150 if o, err := tf.Seek(0, io.SeekStart); err != nil || o != 0 { 151 return nil, err 152 } 153 gz, err := gzip.NewReader(tf) 154 if err != nil { 155 return nil, fmt.Errorf("failed to create gzip reader: %v", err) 156 } 157 158 zlog.Debug(ctx).Msg("success") 159 success = true 160 return &gzippedFile{ 161 Reader: gz, 162 Closer: tf, 163 }, nil 164 } 165 166 zlog.Error(ctx).Msg("exhausted all mirrors") 167 return nil, fmt.Errorf("all update_info mirrors failed to return a response") 168 } 169 170 // gzippedFile implements io.ReadCloser by proxying calls to different 171 // underlying implementations. This is used to make sure the file that backs the 172 // downloaded security database has Close called on it. 173 type gzippedFile struct { 174 io.Reader 175 io.Closer 176 } 177 178 func (c *Client) getMirrors(ctx context.Context, list string) error { 179 ctx = zlog.ContextWithValues(ctx, "component", "aws/Client.getMirrors") 180 181 req, err := http.NewRequestWithContext(ctx, http.MethodGet, list, nil) 182 if err != nil { 183 return fmt.Errorf("failed to create request for mirror list: %v", err) 184 } 185 resp, err := c.c.Do(req) 186 if err != nil { 187 return fmt.Errorf("failed to make request for mirrors: %v", err) 188 } 189 defer resp.Body.Close() 190 191 switch resp.StatusCode { 192 case http.StatusOK: 193 // break 194 default: 195 return fmt.Errorf("failed to make request for mirrors: unexpected response %d %s", resp.StatusCode, resp.Status) 196 } 197 198 if err := ctx.Err(); err != nil { 199 return err 200 } 201 202 body, err := io.ReadAll(resp.Body) 203 if err != nil { 204 return fmt.Errorf("failed to read http body: %v", err) 205 } 206 207 b := strings.TrimSuffix(string(body), "\n") 208 urls := strings.Split(b, "\n") 209 210 for _, u := range urls { 211 uu, err := url.Parse(u) 212 if err != nil { 213 return fmt.Errorf("could not parse returned mirror %v as URL: %v", u, err) 214 } 215 c.mirrors = append(c.mirrors, uu) 216 } 217 218 zlog.Debug(ctx). 219 Msg("successfully got list of mirrors") 220 return nil 221 }