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 }