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