github.com/openshift-online/ocm-sdk-go@v0.1.473/dump.go (about) 1 /* 2 Copyright (c) 2018 Red Hat, Inc. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 // This file contains the implementations of the methods of the connection that are used to dump to 18 // the log the details of HTTP requests and responses. 19 20 package sdk 21 22 import ( 23 "bytes" 24 "context" 25 "io" 26 "mime" 27 "net/http" 28 "net/url" 29 "sort" 30 "strings" 31 32 jsoniter "github.com/json-iterator/go" 33 34 "github.com/openshift-online/ocm-sdk-go/helpers" 35 "github.com/openshift-online/ocm-sdk-go/logging" 36 ) 37 38 // dumpTransportWrapper is a transport wrapper that creates round trippers that dump the details of 39 // the request and the responses to the log. 40 type dumpTransportWrapper struct { 41 logger logging.Logger 42 } 43 44 // Wrap creates a round tripper on top of the given one that sends to the log the details of 45 // requests and responses. 46 func (w *dumpTransportWrapper) Wrap(transport http.RoundTripper) http.RoundTripper { 47 return &dumpRoundTripper{ 48 logger: w.logger, 49 next: transport, 50 } 51 } 52 53 // dumpRoundTripper is a round tripper that dumps the details of the requests and the responses to 54 // the log. 55 type dumpRoundTripper struct { 56 logger logging.Logger 57 next http.RoundTripper 58 } 59 60 // Make sure that we implement the http.RoundTripper interface: 61 var _ http.RoundTripper = &dumpRoundTripper{} 62 63 // RoundTrip is he implementation of the http.RoundTripper interface. 64 func (d *dumpRoundTripper) RoundTrip(request *http.Request) (response *http.Response, err error) { 65 // Get the context: 66 ctx := request.Context() 67 68 // Read the complete body in memory, in order to send it to the log, and replace it with a 69 // reader that reads it from memory: 70 if request.Body != nil { 71 var body []byte 72 body, err = io.ReadAll(request.Body) 73 if err != nil { 74 return 75 } 76 err = request.Body.Close() 77 if err != nil { 78 return 79 } 80 d.dumpRequest(ctx, request, body) 81 request.Body = io.NopCloser(bytes.NewBuffer(body)) 82 } else { 83 d.dumpRequest(ctx, request, nil) 84 } 85 86 // Call the next round tripper: 87 response, err = d.next.RoundTrip(request) 88 if err != nil { 89 return 90 } 91 92 // Read the complete response body in memory, in order to send it the log, and replace it 93 // with a reader that reads it from memory: 94 if response.Body != nil { 95 var body []byte 96 body, err = io.ReadAll(response.Body) 97 if err != nil { 98 return 99 } 100 err = response.Body.Close() 101 if err != nil { 102 return 103 } 104 d.dumpResponse(ctx, response, body) 105 response.Body = io.NopCloser(bytes.NewBuffer(body)) 106 } else { 107 d.dumpResponse(ctx, response, nil) 108 } 109 110 return 111 } 112 113 const ( 114 // redactionStr replaces sensitive values in output. 115 redactionStr = "***" 116 ) 117 118 // redactFields are removed from log output when dumped. 119 var redactFields = map[string]bool{ 120 "access_token": true, 121 "admin": true, 122 "id_token": true, 123 "refresh_token": true, 124 "password": true, 125 "client_secret": true, 126 "kubeconfig": true, 127 "ssh": true, 128 "access_key_id": true, 129 "secret_access_key": true, 130 } 131 132 // dumpRequest dumps to the log, in debug level, the details of the given HTTP request. 133 func (d *dumpRoundTripper) dumpRequest(ctx context.Context, request *http.Request, body []byte) { 134 d.logger.Debug(ctx, "Request method is %s", request.Method) 135 d.logger.Debug(ctx, "Request URL is '%s'", request.URL) 136 if request.Host != "" { 137 d.logger.Debug(ctx, "Request header 'Host' is '%s'", request.Host) 138 } 139 header := request.Header 140 names := make([]string, len(header)) 141 i := 0 142 for name := range header { 143 names[i] = name 144 i++ 145 } 146 sort.Strings(names) 147 for _, name := range names { 148 values := header[name] 149 for _, value := range values { 150 if strings.ToLower(name) == "authorization" { 151 d.logger.Debug(ctx, "Request header '%s' is omitted", name) 152 } else { 153 d.logger.Debug(ctx, "Request header '%s' is '%s'", name, value) 154 } 155 } 156 } 157 if body != nil { 158 d.logger.Debug(ctx, "Request body follows") 159 d.dumpBody(ctx, header, body) 160 } 161 } 162 163 // dumpResponse dumps to the log, in debug level, the details of the given HTTP response. 164 func (d *dumpRoundTripper) dumpResponse(ctx context.Context, response *http.Response, body []byte) { 165 d.logger.Debug(ctx, "Response protocol is '%s'", response.Proto) 166 d.logger.Debug(ctx, "Response status is '%s'", response.Status) 167 header := response.Header 168 names := make([]string, len(header)) 169 i := 0 170 for name := range header { 171 names[i] = name 172 i++ 173 } 174 sort.Strings(names) 175 for _, name := range names { 176 values := header[name] 177 for _, value := range values { 178 d.logger.Debug(ctx, "Response header '%s' is '%s'", name, value) 179 } 180 } 181 if body != nil { 182 d.logger.Debug(ctx, "Response body follows") 183 d.dumpBody(ctx, header, body) 184 } 185 } 186 187 // dumpBody checks the content type used in the given header and then it dumps the given body in a 188 // format suitable for that content type. 189 func (d *dumpRoundTripper) dumpBody(ctx context.Context, header http.Header, body []byte) { 190 // Try to parse the content type: 191 var mediaType string 192 contentType := header.Get("Content-Type") 193 if contentType != "" { 194 var err error 195 mediaType, _, err = mime.ParseMediaType(contentType) 196 if err != nil { 197 d.logger.Error(ctx, "Can't parse content type '%s': %v", contentType, err) 198 } 199 } else { 200 mediaType = contentType 201 } 202 203 // Dump the body according to the content type: 204 switch mediaType { 205 case "application/x-www-form-urlencoded": 206 d.dumpForm(ctx, body) 207 case "application/json", "": 208 d.dumpJSON(ctx, body) 209 default: 210 d.dumpBytes(ctx, body) 211 } 212 } 213 214 // dumpForm sends to the log the contents of the given form data, excluding security sensitive 215 // fields. 216 func (d *dumpRoundTripper) dumpForm(ctx context.Context, data []byte) { 217 // Parse the form: 218 form, err := url.ParseQuery(string(data)) 219 if err != nil { 220 d.dumpBytes(ctx, data) 221 return 222 } 223 224 // Redact values corresponding to security sensitive fields: 225 for name, values := range form { 226 if redactFields[name] { 227 for i := range values { 228 values[i] = redactionStr 229 } 230 } 231 } 232 233 // Get and sort the names of the fields of the form, so that the generated output will be 234 // predictable: 235 names := make([]string, 0, len(form)) 236 for name := range form { 237 names = append(names, name) 238 } 239 sort.Strings(names) 240 241 // Write the names and values to the buffer while redacting the sensitive fields: 242 buffer := &bytes.Buffer{} 243 for _, name := range names { 244 key := url.QueryEscape(name) 245 values := form[name] 246 for _, value := range values { 247 var redacted string 248 if redactFields[name] { 249 redacted = redactionStr 250 } else { 251 redacted = url.QueryEscape(value) 252 } 253 if buffer.Len() > 0 { 254 buffer.WriteByte('&') // #nosec G104 255 } 256 buffer.WriteString(key) // #nosec G104 257 buffer.WriteByte('=') // #nosec G104 258 buffer.WriteString(redacted) // #nosec G104 259 } 260 } 261 262 // Send the redacted data to the log: 263 d.dumpBytes(ctx, buffer.Bytes()) 264 } 265 266 // dumpJSON tries to parse the given data as a JSON document. If that works, then it dumps it 267 // indented, otherwise dumps it as is. 268 func (d *dumpRoundTripper) dumpJSON(ctx context.Context, data []byte) { 269 it, err := helpers.NewIterator(data) 270 if err != nil { 271 d.logger.Debug(ctx, "%s", data) 272 } else { 273 var buf bytes.Buffer 274 str := helpers.NewStream(&buf) 275 276 // remove sensitive information 277 d.redactSensitive(it, str) 278 279 err := str.Flush() 280 if err != nil { 281 d.logger.Debug(ctx, "%s", data) 282 } else { 283 d.logger.Debug(ctx, "%s", buf.String()) 284 } 285 } 286 } 287 288 // dumpBytes dump the given data as an array of bytes. 289 func (d *dumpRoundTripper) dumpBytes(ctx context.Context, data []byte) { 290 d.logger.Debug(ctx, "%s", data) 291 } 292 293 // redactSensitive replaces sensitive fields within a response with redactionStr. 294 func (d *dumpRoundTripper) redactSensitive(it *jsoniter.Iterator, str *jsoniter.Stream) { 295 switch it.WhatIsNext() { 296 case jsoniter.ObjectValue: 297 str.WriteObjectStart() 298 first := true 299 for field := it.ReadObject(); field != ""; field = it.ReadObject() { 300 if !first { 301 str.WriteMore() 302 } 303 first = false 304 str.WriteObjectField(field) 305 if v, ok := redactFields[field]; ok && v { 306 str.WriteString(redactionStr) 307 it.Skip() 308 continue 309 } 310 d.redactSensitive(it, str) 311 } 312 str.WriteObjectEnd() 313 case jsoniter.ArrayValue: 314 str.WriteArrayStart() 315 first := true 316 for it.ReadArray() { 317 if !first { 318 str.WriteMore() 319 } 320 first = false 321 d.redactSensitive(it, str) 322 } 323 str.WriteArrayEnd() 324 case jsoniter.StringValue: 325 str.WriteString(it.ReadString()) 326 case jsoniter.NumberValue: 327 v := it.ReadNumber() 328 i, err := v.Int64() 329 if err == nil { 330 str.WriteInt64(i) 331 break 332 } 333 f, err := v.Float64() 334 if err == nil { 335 str.WriteFloat64(f) 336 } 337 case jsoniter.BoolValue: 338 str.WriteBool(it.ReadBool()) 339 case jsoniter.NilValue: 340 str.WriteNil() 341 // Skip because no reading from it is involved 342 it.Skip() 343 } 344 }