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 }