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  }