github.com/vmware/go-vcloud-director/v2@v2.24.0/util/logging.go (about) 1 /* 2 * Copyright 2019 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. 3 */ 4 5 // Package util provides ancillary functionality to go-vcloud-director library 6 // logging.go regulates logging for the whole library. 7 // See LOGGING.md for detailed usage 8 package util 9 10 import ( 11 "fmt" 12 "io" 13 "log" 14 "net/http" 15 "os" 16 "path" 17 "path/filepath" 18 "regexp" 19 "runtime" 20 "strings" 21 ) 22 23 const ( 24 // Name of the environment variable that enables logging 25 envUseLog = "GOVCD_LOG" 26 27 // envOverwriteLog allows to overwrite file on every initialization 28 envOverwriteLog = "GOVCD_LOG_OVERWRITE" 29 30 // Name of the environment variable with the log file name 31 envLogFileName = "GOVCD_LOG_FILE" 32 33 // Name of the environment variable with the screen output 34 envLogOnScreen = "GOVCD_LOG_ON_SCREEN" 35 36 // Name of the environment variable that enables logging of passwords 37 // #nosec G101 -- This is not a password 38 envLogPasswords = "GOVCD_LOG_PASSWORDS" 39 40 // Name of the environment variable that enables logging of HTTP requests 41 envLogSkipHttpReq = "GOVCD_LOG_SKIP_HTTP_REQ" 42 43 // Name of the environment variable that enables logging of HTTP responses 44 // #nosec G101 -- Not a credential 45 envLogSkipHttpResp = "GOVCD_LOG_SKIP_HTTP_RESP" 46 47 // Name of the environment variable with a custom list of of responses to skip from logging 48 envLogSkipTagList = "GOVCD_LOG_SKIP_TAGS" 49 50 // Name of the environment variable with a custom list of of functions to include in the logging 51 envApiLogFunctions = "GOVCD_LOG_FUNCTIONS" 52 ) 53 54 var ( 55 // All go-vcloud-director logging goes through this logger 56 Logger *log.Logger 57 58 // It's true if we're using an user provided logger 59 customLogging bool = false 60 61 // Name of the log file 62 // activated by GOVCD_LOG_FILE 63 ApiLogFileName string = "go-vcloud-director.log" 64 65 // Globally enabling logs 66 // activated by GOVCD_LOG 67 EnableLogging bool = false 68 69 // OverwriteLog specifies if log file should be overwritten on every run 70 OverwriteLog bool = false 71 72 // Enable logging of passwords 73 // activated by GOVCD_LOG_PASSWORDS 74 LogPasswords bool = false 75 76 // Enable logging of Http requests 77 // disabled by GOVCD_LOG_SKIP_HTTP_REQ 78 LogHttpRequest bool = true 79 80 // Enable logging of Http responses 81 // disabled by GOVCD_LOG_SKIP_HTTP_RESP 82 LogHttpResponse bool = true 83 84 // List of tags to be excluded from logging 85 skipTags = []string{"ovf:License"} 86 87 // List of functions included in logging 88 // If this variable is filled, only operations from matching function names will be logged 89 apiLogFunctions []string 90 91 // Sends log to screen. If value is either "stderr" or "err" 92 // logging will go to os.Stderr. For any other value it will 93 // go to os.Stdout 94 LogOnScreen string = "" 95 96 // Flag indicating that a log file is open 97 // logOpen bool = false 98 99 // PanicEmptyUserAgent will panic if Request header does not have HTTP User-Agent set This 100 // is generally useful in tests and is off by default. 101 PanicEmptyUserAgent bool = false 102 103 // Text lines used for logging of http requests and responses 104 lineLength int = 80 105 dashLine string = strings.Repeat("-", lineLength) 106 hashLine string = strings.Repeat("#", lineLength) 107 ) 108 109 // TogglePanicEmptyUserAgent allows to enable Panic in test if HTTP User-Agent is missing. This 110 // generally is useful in tests and is off by default. 111 func TogglePanicEmptyUserAgent(willPanic bool) { 112 PanicEmptyUserAgent = willPanic 113 } 114 115 func newLogger(logpath string) *log.Logger { 116 var err error 117 var file *os.File 118 if OverwriteLog { 119 file, err = os.OpenFile(filepath.Clean(logpath), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) 120 } else { 121 file, err = os.OpenFile(filepath.Clean(logpath), os.O_RDWR|os.O_CREATE|os.O_APPEND, 0600) 122 } 123 124 if err != nil { 125 fmt.Printf("error opening log file %s : %v", logpath, err) 126 os.Exit(1) 127 } 128 return log.New(file, "", log.Ldate|log.Ltime) 129 } 130 131 func SetCustomLogger(customLogger *log.Logger) { 132 Logger = customLogger 133 EnableLogging = true 134 customLogging = true 135 } 136 137 // initializes logging with known parameters 138 func SetLog() { 139 if customLogging { 140 return 141 } 142 if !EnableLogging { 143 Logger = log.New(io.Discard, "", log.Ldate|log.Ltime) 144 return 145 } 146 147 // If no file name was set, logging goes to the screen 148 if ApiLogFileName == "" { 149 if LogOnScreen == "stderr" || LogOnScreen == "err" { 150 log.SetOutput(os.Stderr) 151 Logger = log.New(os.Stderr, "", log.Ldate|log.Ltime) 152 } else { 153 Logger = log.New(os.Stdout, "", log.Ldate|log.Ltime) 154 } 155 } else { 156 Logger = newLogger(ApiLogFileName) 157 } 158 if len(skipTags) > 0 { 159 Logger.Printf("### WILL SKIP THE FOLLOWING TAGS: %+v", skipTags) 160 } 161 if len(apiLogFunctions) > 0 { 162 Logger.Printf("### WILL ONLY INCLUDE API LOGS FROM THE FOLLOWING FUNCTIONS: %+v", apiLogFunctions) 163 } 164 } 165 166 // hideSensitive hides passwords, tokens, and certificate details 167 func hideSensitive(in string, onScreen bool) string { 168 if !onScreen && LogPasswords { 169 return in 170 } 171 var out string 172 173 // Filters out the below: 174 // Regular passwords 175 re1 := regexp.MustCompile(`("[^\"]*[Pp]assword"\s*:\s*)"[^\"]+"`) 176 out = re1.ReplaceAllString(in, `${1}"********"`) 177 178 // Replace password in ADFS SAML request 179 re2 := regexp.MustCompile(`(\s*<o:Password.*ext">)(.*)(</o:Password>)`) 180 out = re2.ReplaceAllString(out, `${1}******${3}`) 181 182 // Token data between <e:CipherValue> </e:CipherValue> 183 re3 := regexp.MustCompile(`(.*<e:CipherValue>)(.*)(</e:CipherValue>.*)`) 184 out = re3.ReplaceAllString(out, `${1}******${3}`) 185 // Token data between <xenc:CipherValue> </xenc:CipherValue> 186 re4 := regexp.MustCompile(`(.*<xenc:CipherValue>)(.*)(</xenc:CipherValue>.*)`) 187 out = re4.ReplaceAllString(out, `${1}******${3}`) 188 189 // Data inside certificates and private keys 190 re5 := regexp.MustCompile(`(-----BEGIN CERTIFICATE-----)(.*)(-----END CERTIFICATE-----)`) 191 out = re5.ReplaceAllString(out, `${1}******${3}`) 192 re6 := regexp.MustCompile(`(-----BEGIN ENCRYPTED PRIVATE KEY-----)(.*)(-----END ENCRYPTED PRIVATE KEY-----)`) 193 out = re6.ReplaceAllString(out, `${1}******${3}`) 194 195 // Token inside request body 196 re7 := regexp.MustCompile(`(refresh_token)=(\S+)`) 197 out = re7.ReplaceAllString(out, `${1}=*******`) 198 199 // Bearer token inside JSON response 200 re8 := regexp.MustCompile(`("access_token":\s*)"[^"]*`) 201 out = re8.ReplaceAllString(out, `${1}*******`) 202 203 // Token inside JSON response 204 re9 := regexp.MustCompile(`("refresh_token":\s*)"[^"]*`) 205 out = re9.ReplaceAllString(out, `${1}*******`) 206 207 // API Token inside CSE JSON payloads 208 re10 := regexp.MustCompile(`("apiToken":\s*)"[^"]*`) 209 out = re10.ReplaceAllString(out, `${1}*******`) 210 211 return out 212 } 213 214 // Determines whether a string is likely to contain binary data 215 func isBinary(data string, req *http.Request) bool { 216 reContentRange := regexp.MustCompile(`(?i)content-range`) 217 reMultipart := regexp.MustCompile(`(?i)multipart/form`) 218 reMediaXml := regexp.MustCompile(`(?i)media+xml;`) 219 // Skip data transferred for vApp template or catalog item upload 220 if strings.Contains(req.URL.String(), "/transfer/") && 221 (strings.HasSuffix(req.URL.String(), ".vmdk") || strings.HasSuffix(req.URL.String(), "/file")) && 222 (req.Method == http.MethodPut || req.Method == http.MethodPost) { 223 return true 224 } 225 uiPlugin := regexp.MustCompile(`manifest\.json|bundle\.js`) 226 for key, value := range req.Header { 227 if reContentRange.MatchString(key) { 228 return true 229 } 230 if reMultipart.MatchString(key) { 231 return true 232 } 233 for _, v := range value { 234 if reMediaXml.MatchString(v) { 235 return true 236 } 237 } 238 } 239 return uiPlugin.MatchString(data) 240 } 241 242 // SanitizedHeader returns a http.Header with sensitive fields masked 243 func SanitizedHeader(inputHeader http.Header) http.Header { 244 if LogPasswords { 245 return inputHeader 246 } 247 var sensitiveKeys = []string{ 248 "Config-Secret", 249 "Authorization", 250 "X-Vcloud-Authorization", 251 "X-Vmware-Vcloud-Access-Token", 252 } 253 var sanitizedHeader = make(http.Header) 254 for key, value := range inputHeader { 255 // Explicitly mask only token in SIGN token so that other details are not obfuscated 256 // Header format: SIGN token="`+base64GzippedSignToken+`",org="`+org+`" 257 if (key == "authorization" || key == "Authorization") && len(value) == 1 && 258 strings.HasPrefix(value[0], "SIGN") && !LogPasswords { 259 260 re := regexp.MustCompile(`(SIGN token=")([^"]*)(.*)`) 261 out := re.ReplaceAllString(value[0], `${1}********${3}"`) 262 263 Logger.Printf("\t%s: %s\n", key, out) 264 // Do not perform any post processing on this header 265 continue 266 } 267 268 for _, sk := range sensitiveKeys { 269 if strings.EqualFold(sk, key) { 270 value = []string{"********"} 271 break 272 } 273 } 274 sanitizedHeader[key] = value 275 } 276 return sanitizedHeader 277 } 278 279 // logSanitizedHeader logs the contents of the header after sanitizing 280 func logSanitizedHeader(inputHeader http.Header) { 281 for key, value := range SanitizedHeader(inputHeader) { 282 Logger.Printf("\t%s: %s\n", key, value) 283 } 284 } 285 286 // Returns true if the caller function matches any of the functions in the include function list 287 func includeFunction(caller string) bool { 288 if len(apiLogFunctions) > 0 { 289 for _, f := range apiLogFunctions { 290 reFunc := regexp.MustCompile(f) 291 if reFunc.MatchString(caller) { 292 return true 293 } 294 } 295 } else { 296 // If there is no include list, we include everything 297 return true 298 } 299 // If we reach this point, none of the functions in the list matches the caller name 300 return false 301 } 302 303 // Logs the essentials of a HTTP request 304 func ProcessRequestOutput(caller, operation, url, payload string, req *http.Request) { 305 // Special behavior for testing that all requests get HTTP User-Agent set 306 if PanicEmptyUserAgent && req.Header.Get("User-Agent") == "" { 307 panic(fmt.Sprintf("empty User-Agent detected in API call to '%s'", url)) 308 } 309 310 if !LogHttpRequest { 311 return 312 } 313 if !includeFunction(caller) { 314 return 315 } 316 317 Logger.Printf("%s\n", dashLine) 318 Logger.Printf("Request caller: %s\n", caller) 319 Logger.Printf("%s %s\n", operation, url) 320 Logger.Printf("%s\n", dashLine) 321 dataSize := len(payload) 322 if isBinary(payload, req) { 323 payload = "[binary data]" 324 } 325 // Request header should be shown before Request data 326 Logger.Printf("Req header:\n") 327 logSanitizedHeader(req.Header) 328 329 if dataSize > 0 { 330 Logger.Printf("Request data: [%d]\n%s\n", dataSize, hideSensitive(payload, false)) 331 } 332 } 333 334 // Logs the essentials of a HTTP response 335 func ProcessResponseOutput(caller string, resp *http.Response, result string) { 336 if !LogHttpResponse { 337 return 338 } 339 340 if !includeFunction(caller) { 341 return 342 } 343 344 outText := result 345 if len(skipTags) > 0 { 346 for _, longTag := range skipTags { 347 initialTag := `<` + longTag + `.*>` 348 finalTag := `</` + longTag + `>` 349 reInitialSearchTag := regexp.MustCompile(initialTag) 350 351 // The `(?s)` flag treats the regular expression as a single line. 352 // In this context, the dot matches every character until the next operator 353 // The `.*?` is a non-greedy match of every character until the next operator, but 354 // only matching the shortest possible portion. 355 reSearchBothTags := regexp.MustCompile(`(?s)` + initialTag + `.*?` + finalTag) 356 outRepl := fmt.Sprintf("[SKIPPING '%s' TAG AT USER'S REQUEST]", longTag) 357 // We search for the initial long tag 358 if reInitialSearchTag.MatchString(outText) { 359 // If the first tag was found, we search the text to skip the whole output between the tags 360 // Notice that if the second tag is not found, there won't be any replacement 361 outText = reSearchBothTags.ReplaceAllString(outText, outRepl) 362 break 363 } 364 } 365 } 366 Logger.Printf("%s\n", hashLine) 367 Logger.Printf("Response caller %s\n", caller) 368 Logger.Printf("Response status %s\n", resp.Status) 369 Logger.Printf("%s\n", hashLine) 370 Logger.Printf("Response header:\n") 371 logSanitizedHeader(resp.Header) 372 dataSize := len(result) 373 outTextSize := len(outText) 374 if outTextSize != dataSize { 375 Logger.Printf("Response text: [%d -> %d]\n%s\n", dataSize, outTextSize, hideSensitive(outText, false)) 376 } else { 377 Logger.Printf("Response text: [%d]\n%s\n", dataSize, hideSensitive(outText, false)) 378 } 379 } 380 381 // Sets the list of tags to skip 382 func SetSkipTags(tags string) { 383 if tags != "" { 384 skipTags = strings.Split(tags, ",") 385 } 386 } 387 388 // Sets the list of functions to include 389 func SetApiLogFunctions(functions string) { 390 if functions != "" { 391 apiLogFunctions = strings.Split(functions, ",") 392 } 393 } 394 395 // Initializes default logging values 396 func InitLogging() { 397 if os.Getenv(envLogSkipHttpReq) != "" { 398 LogHttpRequest = false 399 } 400 401 if os.Getenv(envLogSkipHttpResp) != "" { 402 LogHttpResponse = false 403 } 404 405 if os.Getenv(envApiLogFunctions) != "" { 406 SetApiLogFunctions(os.Getenv(envApiLogFunctions)) 407 } 408 409 if os.Getenv(envLogSkipTagList) != "" { 410 SetSkipTags(os.Getenv(envLogSkipTagList)) 411 } 412 413 if os.Getenv(envLogPasswords) != "" { 414 EnableLogging = true 415 LogPasswords = true 416 } 417 418 if os.Getenv(envLogFileName) != "" { 419 EnableLogging = true 420 ApiLogFileName = os.Getenv(envLogFileName) 421 } 422 423 LogOnScreen = os.Getenv(envLogOnScreen) 424 if LogOnScreen != "" { 425 ApiLogFileName = "" 426 EnableLogging = true 427 } 428 429 if EnableLogging || os.Getenv(envUseLog) != "" { 430 EnableLogging = true 431 } 432 433 if os.Getenv(envOverwriteLog) != "" { 434 OverwriteLog = true 435 } 436 437 SetLog() 438 } 439 440 func init() { 441 InitLogging() 442 } 443 444 // Returns the name of the function that called the 445 // current function. 446 // Used by functions that call processResponseOutput and 447 // processRequestOutput 448 func CallFuncName() string { 449 fpcs := make([]uintptr, 1) 450 n := runtime.Callers(3, fpcs) 451 if n > 0 { 452 fun := runtime.FuncForPC(fpcs[0] - 1) 453 if fun != nil { 454 return fun.Name() 455 } 456 } 457 return "" 458 } 459 460 // Returns the name of the current function 461 func CurrentFuncName() string { 462 fpcs := make([]uintptr, 1) 463 runtime.Callers(2, fpcs) 464 fun := runtime.FuncForPC(fpcs[0]) 465 return fun.Name() 466 } 467 468 // Returns a string containing up to 10 function names 469 // from the call stack 470 func FuncNameCallStack() string { 471 // Gets the list of function names from the call stack 472 fpcs := make([]uintptr, 10) 473 runtime.Callers(0, fpcs) 474 // Removes the function names from the reflect stack itself and the ones from the API management 475 removeReflect := regexp.MustCompile(`^ runtime.call|reflect.Value|\bNewRequest\b|NewRequestWitNotEncodedParamsWithApiVersion|NewRequestWitNotEncodedParams|ExecuteRequest|ExecuteRequestWithoutResponse|ExecuteTaskRequest`) 476 var stackStr []string 477 // Gets up to 10 functions from the stack 478 for N := 0; N < len(fpcs) && N < 10; N++ { 479 fun := runtime.FuncForPC(fpcs[N]) 480 funcName := path.Base(fun.Name()) 481 if !removeReflect.MatchString(funcName) { 482 stackStr = append(stackStr, funcName) 483 } 484 } 485 // Reverses the function names stack, to make it easier to read 486 var inverseStackStr []string 487 for N := len(stackStr) - 1; N > 1; N-- { 488 if stackStr[N] != "" && stackStr[N] != "." { 489 inverseStackStr = append(inverseStackStr, stackStr[N]) 490 } 491 } 492 return strings.Join(inverseStackStr, "-->") 493 }