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  }