github.com/gophercloud/gophercloud@v1.11.0/internal/acceptance/clients/http.go (about)

     1  package clients
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io"
     8  	"io/ioutil"
     9  	"log"
    10  	"net/http"
    11  	"sort"
    12  	"strings"
    13  )
    14  
    15  // List of headers that need to be redacted
    16  var REDACT_HEADERS = []string{"x-auth-token", "x-auth-key", "x-service-token",
    17  	"x-storage-token", "x-account-meta-temp-url-key", "x-account-meta-temp-url-key-2",
    18  	"x-container-meta-temp-url-key", "x-container-meta-temp-url-key-2", "set-cookie",
    19  	"x-subject-token"}
    20  
    21  // LogRoundTripper satisfies the http.RoundTripper interface and is used to
    22  // customize the default http client RoundTripper to allow logging.
    23  type LogRoundTripper struct {
    24  	Rt http.RoundTripper
    25  }
    26  
    27  // RoundTrip performs a round-trip HTTP request and logs relevant information
    28  // about it.
    29  func (lrt *LogRoundTripper) RoundTrip(request *http.Request) (*http.Response, error) {
    30  	defer func() {
    31  		if request.Body != nil {
    32  			request.Body.Close()
    33  		}
    34  	}()
    35  
    36  	var err error
    37  
    38  	log.Printf("[DEBUG] OpenStack Request URL: %s %s", request.Method, request.URL)
    39  	log.Printf("[DEBUG] OpenStack request Headers:\n%s", formatHeaders(request.Header))
    40  
    41  	if request.Body != nil {
    42  		request.Body, err = lrt.logRequest(request.Body, request.Header.Get("Content-Type"))
    43  		if err != nil {
    44  			return nil, err
    45  		}
    46  	}
    47  
    48  	response, err := lrt.Rt.RoundTrip(request)
    49  	if response == nil {
    50  		return nil, err
    51  	}
    52  
    53  	log.Printf("[DEBUG] OpenStack Response Code: %d", response.StatusCode)
    54  	log.Printf("[DEBUG] OpenStack Response Headers:\n%s", formatHeaders(response.Header))
    55  
    56  	response.Body, err = lrt.logResponse(response.Body, response.Header.Get("Content-Type"))
    57  
    58  	return response, err
    59  }
    60  
    61  // logRequest will log the HTTP Request details.
    62  // If the body is JSON, it will attempt to be pretty-formatted.
    63  func (lrt *LogRoundTripper) logRequest(original io.ReadCloser, contentType string) (io.ReadCloser, error) {
    64  	defer original.Close()
    65  
    66  	var bs bytes.Buffer
    67  	_, err := io.Copy(&bs, original)
    68  	if err != nil {
    69  		return nil, err
    70  	}
    71  
    72  	// Handle request contentType
    73  	if strings.HasPrefix(contentType, "application/json") {
    74  		debugInfo := lrt.formatJSON(bs.Bytes())
    75  		log.Printf("[DEBUG] OpenStack Request Body: %s", debugInfo)
    76  	}
    77  
    78  	return ioutil.NopCloser(strings.NewReader(bs.String())), nil
    79  }
    80  
    81  // logResponse will log the HTTP Response details.
    82  // If the body is JSON, it will attempt to be pretty-formatted.
    83  func (lrt *LogRoundTripper) logResponse(original io.ReadCloser, contentType string) (io.ReadCloser, error) {
    84  	if strings.HasPrefix(contentType, "application/json") {
    85  		var bs bytes.Buffer
    86  		defer original.Close()
    87  		_, err := io.Copy(&bs, original)
    88  		if err != nil {
    89  			return nil, err
    90  		}
    91  		debugInfo := lrt.formatJSON(bs.Bytes())
    92  		if debugInfo != "" {
    93  			log.Printf("[DEBUG] OpenStack Response Body: %s", debugInfo)
    94  		}
    95  		return ioutil.NopCloser(strings.NewReader(bs.String())), nil
    96  	}
    97  
    98  	log.Printf("[DEBUG] Not logging because OpenStack response body isn't JSON")
    99  	return original, nil
   100  }
   101  
   102  // formatJSON will try to pretty-format a JSON body.
   103  // It will also mask known fields which contain sensitive information.
   104  func (lrt *LogRoundTripper) formatJSON(raw []byte) string {
   105  	var rawData interface{}
   106  
   107  	err := json.Unmarshal(raw, &rawData)
   108  	if err != nil {
   109  		log.Printf("[DEBUG] Unable to parse OpenStack JSON: %s", err)
   110  		return string(raw)
   111  	}
   112  
   113  	data, ok := rawData.(map[string]interface{})
   114  	if !ok {
   115  		pretty, err := json.MarshalIndent(rawData, "", "  ")
   116  		if err != nil {
   117  			log.Printf("[DEBUG] Unable to re-marshal OpenStack JSON: %s", err)
   118  			return string(raw)
   119  		}
   120  
   121  		return string(pretty)
   122  	}
   123  
   124  	// Mask known password fields
   125  	if v, ok := data["auth"].(map[string]interface{}); ok {
   126  		if v, ok := v["identity"].(map[string]interface{}); ok {
   127  			if v, ok := v["password"].(map[string]interface{}); ok {
   128  				if v, ok := v["user"].(map[string]interface{}); ok {
   129  					v["password"] = "***"
   130  				}
   131  			}
   132  			if v, ok := v["application_credential"].(map[string]interface{}); ok {
   133  				v["secret"] = "***"
   134  			}
   135  			if v, ok := v["token"].(map[string]interface{}); ok {
   136  				v["id"] = "***"
   137  			}
   138  		}
   139  	}
   140  
   141  	// Ignore the catalog
   142  	if v, ok := data["token"].(map[string]interface{}); ok {
   143  		if _, ok := v["catalog"]; ok {
   144  			return ""
   145  		}
   146  	}
   147  
   148  	pretty, err := json.MarshalIndent(data, "", "  ")
   149  	if err != nil {
   150  		log.Printf("[DEBUG] Unable to re-marshal OpenStack JSON: %s", err)
   151  		return string(raw)
   152  	}
   153  
   154  	return string(pretty)
   155  }
   156  
   157  // redactHeaders processes a headers object, returning a redacted list
   158  func redactHeaders(headers http.Header) (processedHeaders []string) {
   159  	for name, header := range headers {
   160  		var sensitive bool
   161  
   162  		for _, redact_header := range REDACT_HEADERS {
   163  			if strings.ToLower(name) == strings.ToLower(redact_header) {
   164  				sensitive = true
   165  			}
   166  		}
   167  
   168  		for _, v := range header {
   169  			if sensitive {
   170  				processedHeaders = append(processedHeaders, fmt.Sprintf("%v: %v", name, "***"))
   171  			} else {
   172  				processedHeaders = append(processedHeaders, fmt.Sprintf("%v: %v", name, v))
   173  			}
   174  		}
   175  	}
   176  	return
   177  }
   178  
   179  // formatHeaders processes a headers object plus a deliminator, returning a string
   180  func formatHeaders(headers http.Header) string {
   181  	redactedHeaders := redactHeaders(headers)
   182  	sort.Strings(redactedHeaders)
   183  
   184  	return strings.Join(redactedHeaders, "\n")
   185  }