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 }