mirror of
https://github.com/coreos/ignition.git
synced 2026-02-06 09:47:17 +01:00
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) ```
365 lines
9.2 KiB
Go
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, ",")
|
|
}
|