github.com/vnpaycloud-console/gophercloud/v2@v2.0.5/internal/acceptance/clients/http.go (about)

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