github.com/argoproj/argo-cd/v3@v3.2.1/util/http/http.go (about)

     1  package http
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"io"
     7  	"math"
     8  	"net/http"
     9  	"net/http/httputil"
    10  	"strconv"
    11  	"strings"
    12  	"time"
    13  
    14  	log "github.com/sirupsen/logrus"
    15  	"k8s.io/client-go/transport"
    16  
    17  	"github.com/argoproj/argo-cd/v3/common"
    18  	"github.com/argoproj/argo-cd/v3/util/env"
    19  )
    20  
    21  const (
    22  	maxCookieLength = 4093
    23  
    24  	// limit size of the resp to 512KB
    25  	respReadLimit       = int64(524288)
    26  	retryWaitMax        = time.Duration(10) * time.Second
    27  	EnvRetryMax         = "ARGOCD_K8SCLIENT_RETRY_MAX"
    28  	EnvRetryBaseBackoff = "ARGOCD_K8SCLIENT_RETRY_BASE_BACKOFF"
    29  )
    30  
    31  // max number of chunks a cookie can be broken into. To be compatible with
    32  // widest range of browsers, you shouldn't create more than 30 cookies per domain
    33  var maxCookieNumber = env.ParseNumFromEnv(common.EnvMaxCookieNumber, 20, 0, math.MaxInt)
    34  
    35  // MakeCookieMetadata generates a string representing a Web cookie.  Yum!
    36  func MakeCookieMetadata(key, value string, flags ...string) ([]string, error) {
    37  	attributes := strings.Join(flags, "; ")
    38  
    39  	// cookie: name=value; attributes and key: key-(i) e.g. argocd.token-1
    40  	maxValueLength := maxCookieValueLength(key, attributes)
    41  	numberOfCookies := int(math.Ceil(float64(len(value)) / float64(maxValueLength)))
    42  	if numberOfCookies > maxCookieNumber {
    43  		return nil, fmt.Errorf("the authentication token is %d characters long and requires %d cookies but the max number of cookies is %d. Contact your Argo CD administrator to increase the max number of cookies", len(value), numberOfCookies, maxCookieNumber)
    44  	}
    45  
    46  	return splitCookie(key, value, attributes), nil
    47  }
    48  
    49  // browser has limit on size of cookie, currently 4kb. In order to
    50  // support cookies longer than 4kb, we split cookie into multiple 4kb chunks.
    51  // first chunk will be of format argocd.token=<numberOfChunks>:token; attributes
    52  func splitCookie(key, value, attributes string) []string {
    53  	var cookies []string
    54  	valueLength := len(value)
    55  	// cookie: name=value; attributes and key: key-(i) e.g. argocd.token-1
    56  	maxValueLength := maxCookieValueLength(key, attributes)
    57  	numberOfChunks := int(math.Ceil(float64(valueLength) / float64(maxValueLength)))
    58  
    59  	var end int
    60  	for i, j := 0, 0; i < valueLength; i, j = i+maxValueLength, j+1 {
    61  		end = i + maxValueLength
    62  		if end > valueLength {
    63  			end = valueLength
    64  		}
    65  
    66  		var cookie string
    67  		switch {
    68  		case j == 0 && numberOfChunks == 1:
    69  			cookie = fmt.Sprintf("%s=%s", key, value[i:end])
    70  		case j == 0:
    71  			cookie = fmt.Sprintf("%s=%d:%s", key, numberOfChunks, value[i:end])
    72  		default:
    73  			cookie = fmt.Sprintf("%s-%d=%s", key, j, value[i:end])
    74  		}
    75  		if attributes != "" {
    76  			cookie = fmt.Sprintf("%s; %s", cookie, attributes)
    77  		}
    78  		cookies = append(cookies, cookie)
    79  	}
    80  	return cookies
    81  }
    82  
    83  // JoinCookies combines chunks of cookie based on key as prefix. It returns cookie
    84  // value as string. cookieString is of format key1=value1; key2=value2; key3=value3
    85  // first chunk will be of format argocd.token=<numberOfChunks>:token; attributes
    86  func JoinCookies(key string, cookieList []*http.Cookie) (string, error) {
    87  	cookies := make(map[string]string)
    88  	for _, cookie := range cookieList {
    89  		if !strings.HasPrefix(cookie.Name, key) {
    90  			continue
    91  		}
    92  		cookies[cookie.Name] = cookie.Value
    93  	}
    94  
    95  	var sb strings.Builder
    96  	var numOfChunks int
    97  	var err error
    98  	var token string
    99  	var ok bool
   100  
   101  	if token, ok = cookies[key]; !ok {
   102  		return "", fmt.Errorf("failed to retrieve cookie %s", key)
   103  	}
   104  	parts := strings.Split(token, ":")
   105  
   106  	switch len(parts) {
   107  	case 2:
   108  		if numOfChunks, err = strconv.Atoi(parts[0]); err != nil {
   109  			return "", err
   110  		}
   111  		sb.WriteString(parts[1])
   112  	case 1:
   113  		numOfChunks = 1
   114  		sb.WriteString(parts[0])
   115  	default:
   116  		return "", fmt.Errorf("invalid cookie for key %s", key)
   117  	}
   118  
   119  	for i := 1; i < numOfChunks; i++ {
   120  		sb.WriteString(cookies[fmt.Sprintf("%s-%d", key, i)])
   121  	}
   122  	return sb.String(), nil
   123  }
   124  
   125  func maxCookieValueLength(key, attributes string) int {
   126  	if attributes != "" {
   127  		return maxCookieLength - (len(key) + 3) - (len(attributes) + 2)
   128  	}
   129  	return maxCookieLength - (len(key) + 3)
   130  }
   131  
   132  // DebugTransport is a HTTP Client Transport to enable debugging
   133  type DebugTransport struct {
   134  	T http.RoundTripper
   135  }
   136  
   137  func (d DebugTransport) RoundTrip(req *http.Request) (*http.Response, error) {
   138  	reqDump, err := httputil.DumpRequest(req, true)
   139  	if err != nil {
   140  		return nil, err
   141  	}
   142  	log.Printf("%s", reqDump)
   143  
   144  	resp, err := d.T.RoundTrip(req)
   145  	if err != nil {
   146  		return nil, err
   147  	}
   148  
   149  	respDump, err := httputil.DumpResponse(resp, true)
   150  	if err != nil {
   151  		_ = resp.Body.Close()
   152  		return nil, err
   153  	}
   154  	log.Printf("%s", respDump)
   155  	return resp, nil
   156  }
   157  
   158  // TransportWithHeader is a HTTP Client Transport with default headers.
   159  type TransportWithHeader struct {
   160  	RoundTripper http.RoundTripper
   161  	Header       http.Header
   162  }
   163  
   164  func (rt *TransportWithHeader) RoundTrip(r *http.Request) (*http.Response, error) {
   165  	if rt.Header != nil {
   166  		headers := rt.Header.Clone()
   167  		for k, vs := range r.Header {
   168  			for _, v := range vs {
   169  				headers.Add(k, v)
   170  			}
   171  		}
   172  		r.Header = headers
   173  	}
   174  	return rt.RoundTripper.RoundTrip(r)
   175  }
   176  
   177  func WithRetry(maxRetries int64, baseRetryBackoff time.Duration) transport.WrapperFunc {
   178  	return func(rt http.RoundTripper) http.RoundTripper {
   179  		return &retryTransport{
   180  			inner:      rt,
   181  			maxRetries: maxRetries,
   182  			backoff:    baseRetryBackoff,
   183  		}
   184  	}
   185  }
   186  
   187  type retryTransport struct {
   188  	inner      http.RoundTripper
   189  	maxRetries int64
   190  	backoff    time.Duration
   191  }
   192  
   193  func isRetriable(resp *http.Response) bool {
   194  	if resp == nil {
   195  		return false
   196  	}
   197  	if resp.StatusCode == http.StatusTooManyRequests {
   198  		return true
   199  	}
   200  	if resp.StatusCode == 0 || (resp.StatusCode >= 500 && resp.StatusCode != http.StatusNotImplemented) {
   201  		return true
   202  	}
   203  	return false
   204  }
   205  
   206  func (t *retryTransport) RoundTrip(req *http.Request) (*http.Response, error) {
   207  	var resp *http.Response
   208  	var err error
   209  	backoff := t.backoff
   210  	var bodyBytes []byte
   211  	if req.Body != nil {
   212  		bodyBytes, _ = io.ReadAll(req.Body)
   213  	}
   214  	for i := 0; i <= int(t.maxRetries); i++ {
   215  		req.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
   216  		resp, err = t.inner.RoundTrip(req)
   217  		if i < int(t.maxRetries) && (err != nil || isRetriable(resp)) {
   218  			if resp != nil && resp.Body != nil {
   219  				drainBody(resp.Body)
   220  			}
   221  			if backoff > retryWaitMax {
   222  				backoff = retryWaitMax
   223  			}
   224  			select {
   225  			case <-time.After(backoff):
   226  			case <-req.Context().Done():
   227  				return nil, req.Context().Err()
   228  			}
   229  			backoff *= 2
   230  			continue
   231  		}
   232  		break
   233  	}
   234  	return resp, err
   235  }
   236  
   237  func drainBody(body io.ReadCloser) {
   238  	defer body.Close()
   239  	_, err := io.Copy(io.Discard, io.LimitReader(body, respReadLimit))
   240  	if err != nil {
   241  		log.Warnf("error reading response body: %s", err.Error())
   242  	}
   243  }