1package httpd 2 3import ( 4 "fmt" 5 "net" 6 "net/http" 7 "strconv" 8 "strings" 9 "time" 10 11 "github.com/influxdata/influxql" 12) 13 14// responseLogger is wrapper of http.ResponseWriter that keeps track of its HTTP status 15// code and body size 16type responseLogger struct { 17 w http.ResponseWriter 18 status int 19 size int 20} 21 22func (l *responseLogger) CloseNotify() <-chan bool { 23 if notifier, ok := l.w.(http.CloseNotifier); ok { 24 return notifier.CloseNotify() 25 } 26 // needed for response recorder for testing 27 return make(<-chan bool) 28} 29 30func (l *responseLogger) Header() http.Header { 31 return l.w.Header() 32} 33 34func (l *responseLogger) Flush() { 35 l.w.(http.Flusher).Flush() 36} 37 38func (l *responseLogger) Write(b []byte) (int, error) { 39 if l.status == 0 { 40 // Set status if WriteHeader has not been called 41 l.status = http.StatusOK 42 } 43 44 size, err := l.w.Write(b) 45 l.size += size 46 return size, err 47} 48 49func (l *responseLogger) WriteHeader(s int) { 50 l.w.WriteHeader(s) 51 l.status = s 52} 53 54func (l *responseLogger) Status() int { 55 if l.status == 0 { 56 // This can happen if we never actually write data, but only set response headers. 57 l.status = http.StatusOK 58 } 59 return l.status 60} 61 62func (l *responseLogger) Size() int { 63 return l.size 64} 65 66// redact any occurrence of a password parameter, 'p' 67func redactPassword(r *http.Request) { 68 q := r.URL.Query() 69 if p := q.Get("p"); p != "" { 70 q.Set("p", "[REDACTED]") 71 r.URL.RawQuery = q.Encode() 72 } 73} 74 75// Common Log Format: http://en.wikipedia.org/wiki/Common_Log_Format 76 77// buildLogLine creates a common log format 78// in addition to the common fields, we also append referrer, user agent, 79// request ID and response time (microseconds) 80// ie, in apache mod_log_config terms: 81// %h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-agent}i\"" %L %D 82func buildLogLine(l *responseLogger, r *http.Request, start time.Time) string { 83 84 redactPassword(r) 85 86 username := parseUsername(r) 87 88 host, _, err := net.SplitHostPort(r.RemoteAddr) 89 if err != nil { 90 host = r.RemoteAddr 91 } 92 93 if xff := r.Header["X-Forwarded-For"]; xff != nil { 94 addrs := append(xff, host) 95 host = strings.Join(addrs, ",") 96 } 97 98 uri := r.URL.RequestURI() 99 100 referer := r.Referer() 101 102 userAgent := r.UserAgent() 103 104 return fmt.Sprintf(`%s - %s [%s] "%s %s %s" %s %s "%s" "%s" %s %d`, 105 host, 106 detect(username, "-"), 107 start.Format("02/Jan/2006:15:04:05 -0700"), 108 r.Method, 109 uri, 110 r.Proto, 111 detect(strconv.Itoa(l.Status()), "-"), 112 strconv.Itoa(l.Size()), 113 detect(referer, "-"), 114 detect(userAgent, "-"), 115 r.Header.Get("Request-Id"), 116 // response time, report in microseconds because this is consistent 117 // with apache's %D parameter in mod_log_config 118 int64(time.Since(start)/time.Microsecond)) 119} 120 121// detect detects the first presence of a non blank string and returns it 122func detect(values ...string) string { 123 for _, v := range values { 124 if v != "" { 125 return v 126 } 127 } 128 return "" 129} 130 131// parses the username either from the url or auth header 132func parseUsername(r *http.Request) string { 133 var ( 134 username = "" 135 url = r.URL 136 ) 137 138 // get username from the url if passed there 139 if url.User != nil { 140 if name := url.User.Username(); name != "" { 141 username = name 142 } 143 } 144 145 // Try to get the username from the query param 'u' 146 q := url.Query() 147 if u := q.Get("u"); u != "" { 148 username = u 149 } 150 151 // Try to get it from the authorization header if set there 152 if username == "" { 153 if u, _, ok := r.BasicAuth(); ok { 154 username = u 155 } 156 } 157 return username 158} 159 160// sanitize redacts passwords from query string for logging. 161func sanitize(r *http.Request) { 162 values := r.URL.Query() 163 for i, q := range values["q"] { 164 values["q"][i] = influxql.Sanitize(q) 165 } 166 r.URL.RawQuery = values.Encode() 167} 168