1
0
mirror of https://github.com/coreos/ignition.git synced 2026-02-06 09:47:17 +01:00
Files
ignition/internal/resource/http.go
yasminvalim 2e28ba6b96 internal/resource: Ignore close() errors for http/url
Those errors would be triggered after processing already happened so
they are unlikely and we should not act on them.

Fixes lint:
```
Error: internal/resource/http.go:319:19: Error return value of `resp.Body.Close` is not checked (errcheck)
Error: internal/resource/url.go:340:12: Error return value of `l.Close` is not checked (errcheck)
Error: internal/resource/url.go:380:24: Error return value of `dataReader.Close` is not checked (errcheck)
Error: internal/resource/url.go:620:33: Error return value of `downloadStream.Body.Close` is not checked (errcheck)
Error: internal/resource/url.go:654:26: Error return value of `decompressor.Close` is not checked (errcheck)
```
2025-08-07 12:37:44 +02:00

365 lines
9.2 KiB
Go

// Copyright 2016 CoreOS, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package resource
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/hex"
"encoding/pem"
"errors"
"io"
"net"
"net/http"
"net/url"
"strings"
"time"
ignerrors "github.com/coreos/ignition/v2/config/shared/errors"
"github.com/coreos/ignition/v2/config/v3_6_experimental/types"
"github.com/coreos/ignition/v2/internal/earlyrand"
"github.com/coreos/ignition/v2/internal/log"
"github.com/coreos/ignition/v2/internal/util"
"github.com/coreos/ignition/v2/internal/version"
"github.com/vincent-petithory/dataurl"
"golang.org/x/net/http/httpproxy"
)
const (
initialBackoff = 200 * time.Millisecond
maxBackoff = 5 * time.Second
defaultHttpResponseHeaderTimeout = 10
defaultHttpTotalTimeout = 0
)
var (
ErrTimeout = errors.New("unable to fetch resource in time")
ErrPEMDecodeFailed = errors.New("unable to decode PEM block")
)
// HttpClient is a simple wrapper around the Go HTTP client that standardizes
// the process and logging of fetching payloads.
type HttpClient struct {
client *http.Client
logger *log.Logger
timeout time.Duration
transport *http.Transport
cas map[string][]byte
}
func (f *Fetcher) UpdateHttpTimeoutsAndCAs(timeouts types.Timeouts, cas []types.Resource, proxy types.Proxy) error {
if f.client == nil {
if err := f.newHttpClient(); err != nil {
return err
}
}
// Update timeouts
responseHeader := defaultHttpResponseHeaderTimeout
total := defaultHttpTotalTimeout
if timeouts.HTTPResponseHeaders != nil {
responseHeader = *timeouts.HTTPResponseHeaders
}
if timeouts.HTTPTotal != nil {
total = *timeouts.HTTPTotal
}
f.client.client.Timeout = time.Duration(total) * time.Second
f.client.timeout = f.client.client.Timeout
f.client.transport.ResponseHeaderTimeout = time.Duration(responseHeader) * time.Second
f.client.client.Transport = f.client.transport
// Update proxy
f.client.transport.Proxy = func(req *http.Request) (*url.URL, error) {
return proxyFuncFromIgnitionConfig(proxy)(req.URL)
}
f.client.client.Transport = f.client.transport
// Update CAs
if len(cas) == 0 {
return nil
}
pool, err := x509.SystemCertPool()
if err != nil {
f.Logger.Err("Unable to read system certificate pool: %s", err)
return err
}
for _, ca := range cas {
cablob, err := f.getCABlob(ca)
if err != nil {
return err
}
if err := f.parseCABundle(cablob, ca, pool); err != nil {
f.Logger.Err("Unable to parse CA bundle: %s", err)
return err
}
}
f.client.transport.TLSClientConfig = &tls.Config{RootCAs: pool}
return nil
}
// parseCABundle parses a CA bundle which includes multiple CAs.
func (f *Fetcher) parseCABundle(cablob []byte, ca types.Resource, pool *x509.CertPool) error {
for len(cablob) > 0 {
block, rest := pem.Decode(cablob)
if block == nil {
f.Logger.Err("Unable to decode CA (%v)", ca.Source)
return ErrPEMDecodeFailed
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
f.Logger.Err("Unable to parse CA (%v): %s", ca.Source, err)
return err
}
f.Logger.Info("Adding %q to list of CAs", cert.Subject.CommonName)
pool.AddCert(cert)
cablob = rest
}
return nil
}
func (f *Fetcher) getCABlob(ca types.Resource) ([]byte, error) {
// this is also already checked at validation time
if ca.Source == nil {
f.Logger.Crit("invalid CA: %v", ignerrors.ErrSourceRequired)
return nil, ignerrors.ErrSourceRequired
}
if blob, ok := f.client.cas[*ca.Source]; ok {
return blob, nil
}
u, err := url.Parse(*ca.Source)
if err != nil {
f.Logger.Crit("Unable to parse CA URL: %s", err)
return nil, err
}
hasher, err := util.GetHasher(ca.Verification)
if err != nil {
f.Logger.Crit("Unable to get hasher: %s", err)
return nil, err
}
var expectedSum []byte
if hasher != nil {
// explicitly ignoring the error here because the config should already
// be validated by this point
_, expectedSumString, _ := util.HashParts(ca.Verification)
expectedSum, err = hex.DecodeString(expectedSumString)
if err != nil {
f.Logger.Crit("Error parsing verification string %q: %v", expectedSumString, err)
return nil, err
}
}
var headers http.Header
if len(ca.HTTPHeaders) > 0 {
headers, err = ca.HTTPHeaders.Parse()
if err != nil {
return nil, err
}
}
var compression string
if ca.Compression != nil {
compression = *ca.Compression
}
cablob, err := f.FetchToBuffer(*u, FetchOptions{
Hash: hasher,
Headers: headers,
ExpectedSum: expectedSum,
Compression: compression,
})
if err != nil {
f.Logger.Err("Unable to fetch CA (%s): %s", u, err)
return nil, err
}
f.client.cas[*ca.Source] = cablob
return cablob, nil
}
// RewriteCAsWithDataUrls will modify the passed in slice of CA references to
// contain the actual CA file via a dataurl in their source field.
func (f *Fetcher) RewriteCAsWithDataUrls(cas []types.Resource) error {
for i, ca := range cas {
blob, err := f.getCABlob(ca)
if err != nil {
return err
}
// Clean HTTP headers
cas[i].HTTPHeaders = nil
// the rewrite wipes the compression
cas[i].Compression = nil
encoded := dataurl.EncodeBytes(blob)
cas[i].Source = &encoded
}
return nil
}
// DefaultHTTPClient builds the default `http.client` for Ignition.
func defaultHTTPClient() (*http.Client, error) {
urand, err := earlyrand.UrandomReader()
if err != nil {
return nil, err
}
tlsConfig := tls.Config{
Rand: urand,
}
transport := http.Transport{
ResponseHeaderTimeout: time.Duration(defaultHttpResponseHeaderTimeout) * time.Second,
Dial: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
Resolver: &net.Resolver{
PreferGo: true,
},
}).Dial,
TLSClientConfig: &tlsConfig,
TLSHandshakeTimeout: 10 * time.Second,
}
client := http.Client{
Transport: &transport,
}
return &client, nil
}
// newHttpClient populates the fetcher with the default HTTP client.
func (f *Fetcher) newHttpClient() error {
defaultClient, err := defaultHTTPClient()
if err != nil {
return err
}
f.client = &HttpClient{
client: defaultClient,
logger: f.Logger,
timeout: time.Duration(defaultHttpTotalTimeout) * time.Second,
transport: defaultClient.Transport.(*http.Transport),
cas: make(map[string][]byte),
}
return nil
}
func shouldRetryHttp(statusCode int, opts FetchOptions) bool {
// we always retry 500+
if statusCode >= 500 {
return true
}
for _, retryCode := range opts.RetryCodes {
if statusCode == retryCode {
return true
}
}
return false
}
// httpReaderWithHeader performs an HTTP request on the provided URL with the
// provided request header & method and returns the response body Reader, HTTP
// status code, a cancel function for the result's context, and error (if any).
// By default, User-Agent is added to the header but this can be overridden.
func (c HttpClient) httpReaderWithHeader(opts FetchOptions, url string) (io.ReadCloser, int, context.CancelFunc, error) {
if opts.HTTPVerb == "" {
opts.HTTPVerb = "GET"
}
req, err := http.NewRequest(opts.HTTPVerb, url, nil)
if err != nil {
return nil, 0, nil, err
}
req.Header.Set("User-Agent", "Ignition/"+version.Raw)
for key, values := range opts.Headers {
req.Header.Del(key)
for _, value := range values {
req.Header.Add(key, value)
}
}
ctx, cancelFn := context.WithCancel(context.Background())
if c.timeout != 0 {
cancelFn()
ctx, cancelFn = context.WithTimeout(context.Background(), c.timeout)
}
duration := initialBackoff
for attempt := 1; ; attempt++ {
c.logger.Info("%s %s: attempt #%d", opts.HTTPVerb, url, attempt)
resp, err := c.client.Do(req.WithContext(ctx))
if err == nil {
c.logger.Info("%s result: %s", opts.HTTPVerb, http.StatusText(resp.StatusCode))
if !shouldRetryHttp(resp.StatusCode, opts) {
return resp.Body, resp.StatusCode, cancelFn, nil
}
_ = resp.Body.Close()
} else {
c.logger.Info("%s error: %v", opts.HTTPVerb, err)
}
// Wait before next attempt or exit if we timeout while waiting
select {
case <-time.After(duration):
case <-ctx.Done():
return nil, 0, cancelFn, ErrTimeout
}
duration = duration * 2
if duration > maxBackoff {
duration = maxBackoff
}
}
}
func proxyFuncFromIgnitionConfig(proxy types.Proxy) func(*url.URL) (*url.URL, error) {
noProxy := translateNoProxySliceToString(proxy.NoProxy)
if proxy.HTTPProxy == nil {
proxy.HTTPProxy = new(string)
}
if proxy.HTTPSProxy == nil {
proxy.HTTPSProxy = new(string)
}
cfg := &httpproxy.Config{
HTTPProxy: *proxy.HTTPProxy,
HTTPSProxy: *proxy.HTTPSProxy,
NoProxy: noProxy,
}
return cfg.ProxyFunc()
}
func translateNoProxySliceToString(items []types.NoProxyItem) string {
newItems := make([]string, len(items))
for i, o := range items {
newItems[i] = string(o)
}
return strings.Join(newItems, ",")
}