1// Copyright 2013 The Gorilla Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5package handlers
6
7import (
8	"io"
9	"net"
10	"net/http"
11	"net/url"
12	"strconv"
13	"time"
14	"unicode/utf8"
15)
16
17// Logging
18
19// LogFormatterParams is the structure any formatter will be handed when time to log comes
20type LogFormatterParams struct {
21	Request    *http.Request
22	URL        url.URL
23	TimeStamp  time.Time
24	StatusCode int
25	Size       int
26}
27
28// LogFormatter gives the signature of the formatter function passed to CustomLoggingHandler
29type LogFormatter func(writer io.Writer, params LogFormatterParams)
30
31// loggingHandler is the http.Handler implementation for LoggingHandlerTo and its
32// friends
33
34type loggingHandler struct {
35	writer    io.Writer
36	handler   http.Handler
37	formatter LogFormatter
38}
39
40func (h loggingHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
41	t := time.Now()
42	logger := makeLogger(w)
43	url := *req.URL
44
45	h.handler.ServeHTTP(logger, req)
46	if req.MultipartForm != nil {
47		req.MultipartForm.RemoveAll()
48	}
49
50	params := LogFormatterParams{
51		Request:    req,
52		URL:        url,
53		TimeStamp:  t,
54		StatusCode: logger.Status(),
55		Size:       logger.Size(),
56	}
57
58	h.formatter(h.writer, params)
59}
60
61func makeLogger(w http.ResponseWriter) loggingResponseWriter {
62	var logger loggingResponseWriter = &responseLogger{w: w, status: http.StatusOK}
63	if _, ok := w.(http.Hijacker); ok {
64		logger = &hijackLogger{responseLogger{w: w, status: http.StatusOK}}
65	}
66	h, ok1 := logger.(http.Hijacker)
67	c, ok2 := w.(http.CloseNotifier)
68	if ok1 && ok2 {
69		return hijackCloseNotifier{logger, h, c}
70	}
71	if ok2 {
72		return &closeNotifyWriter{logger, c}
73	}
74	return logger
75}
76
77type commonLoggingResponseWriter interface {
78	http.ResponseWriter
79	http.Flusher
80	Status() int
81	Size() int
82}
83
84const lowerhex = "0123456789abcdef"
85
86func appendQuoted(buf []byte, s string) []byte {
87	var runeTmp [utf8.UTFMax]byte
88	for width := 0; len(s) > 0; s = s[width:] {
89		r := rune(s[0])
90		width = 1
91		if r >= utf8.RuneSelf {
92			r, width = utf8.DecodeRuneInString(s)
93		}
94		if width == 1 && r == utf8.RuneError {
95			buf = append(buf, `\x`...)
96			buf = append(buf, lowerhex[s[0]>>4])
97			buf = append(buf, lowerhex[s[0]&0xF])
98			continue
99		}
100		if r == rune('"') || r == '\\' { // always backslashed
101			buf = append(buf, '\\')
102			buf = append(buf, byte(r))
103			continue
104		}
105		if strconv.IsPrint(r) {
106			n := utf8.EncodeRune(runeTmp[:], r)
107			buf = append(buf, runeTmp[:n]...)
108			continue
109		}
110		switch r {
111		case '\a':
112			buf = append(buf, `\a`...)
113		case '\b':
114			buf = append(buf, `\b`...)
115		case '\f':
116			buf = append(buf, `\f`...)
117		case '\n':
118			buf = append(buf, `\n`...)
119		case '\r':
120			buf = append(buf, `\r`...)
121		case '\t':
122			buf = append(buf, `\t`...)
123		case '\v':
124			buf = append(buf, `\v`...)
125		default:
126			switch {
127			case r < ' ':
128				buf = append(buf, `\x`...)
129				buf = append(buf, lowerhex[s[0]>>4])
130				buf = append(buf, lowerhex[s[0]&0xF])
131			case r > utf8.MaxRune:
132				r = 0xFFFD
133				fallthrough
134			case r < 0x10000:
135				buf = append(buf, `\u`...)
136				for s := 12; s >= 0; s -= 4 {
137					buf = append(buf, lowerhex[r>>uint(s)&0xF])
138				}
139			default:
140				buf = append(buf, `\U`...)
141				for s := 28; s >= 0; s -= 4 {
142					buf = append(buf, lowerhex[r>>uint(s)&0xF])
143				}
144			}
145		}
146	}
147	return buf
148
149}
150
151// buildCommonLogLine builds a log entry for req in Apache Common Log Format.
152// ts is the timestamp with which the entry should be logged.
153// status and size are used to provide the response HTTP status and size.
154func buildCommonLogLine(req *http.Request, url url.URL, ts time.Time, status int, size int) []byte {
155	username := "-"
156	if url.User != nil {
157		if name := url.User.Username(); name != "" {
158			username = name
159		}
160	}
161
162	host, _, err := net.SplitHostPort(req.RemoteAddr)
163
164	if err != nil {
165		host = req.RemoteAddr
166	}
167
168	uri := req.RequestURI
169
170	// Requests using the CONNECT method over HTTP/2.0 must use
171	// the authority field (aka r.Host) to identify the target.
172	// Refer: https://httpwg.github.io/specs/rfc7540.html#CONNECT
173	if req.ProtoMajor == 2 && req.Method == "CONNECT" {
174		uri = req.Host
175	}
176	if uri == "" {
177		uri = url.RequestURI()
178	}
179
180	buf := make([]byte, 0, 3*(len(host)+len(username)+len(req.Method)+len(uri)+len(req.Proto)+50)/2)
181	buf = append(buf, host...)
182	buf = append(buf, " - "...)
183	buf = append(buf, username...)
184	buf = append(buf, " ["...)
185	buf = append(buf, ts.Format("02/Jan/2006:15:04:05 -0700")...)
186	buf = append(buf, `] "`...)
187	buf = append(buf, req.Method...)
188	buf = append(buf, " "...)
189	buf = appendQuoted(buf, uri)
190	buf = append(buf, " "...)
191	buf = append(buf, req.Proto...)
192	buf = append(buf, `" `...)
193	buf = append(buf, strconv.Itoa(status)...)
194	buf = append(buf, " "...)
195	buf = append(buf, strconv.Itoa(size)...)
196	return buf
197}
198
199// writeLog writes a log entry for req to w in Apache Common Log Format.
200// ts is the timestamp with which the entry should be logged.
201// status and size are used to provide the response HTTP status and size.
202func writeLog(writer io.Writer, params LogFormatterParams) {
203	buf := buildCommonLogLine(params.Request, params.URL, params.TimeStamp, params.StatusCode, params.Size)
204	buf = append(buf, '\n')
205	writer.Write(buf)
206}
207
208// writeCombinedLog writes a log entry for req to w in Apache Combined Log Format.
209// ts is the timestamp with which the entry should be logged.
210// status and size are used to provide the response HTTP status and size.
211func writeCombinedLog(writer io.Writer, params LogFormatterParams) {
212	buf := buildCommonLogLine(params.Request, params.URL, params.TimeStamp, params.StatusCode, params.Size)
213	buf = append(buf, ` "`...)
214	buf = appendQuoted(buf, params.Request.Referer())
215	buf = append(buf, `" "`...)
216	buf = appendQuoted(buf, params.Request.UserAgent())
217	buf = append(buf, '"', '\n')
218	writer.Write(buf)
219}
220
221// CombinedLoggingHandler return a http.Handler that wraps h and logs requests to out in
222// Apache Combined Log Format.
223//
224// See http://httpd.apache.org/docs/2.2/logs.html#combined for a description of this format.
225//
226// LoggingHandler always sets the ident field of the log to -
227func CombinedLoggingHandler(out io.Writer, h http.Handler) http.Handler {
228	return loggingHandler{out, h, writeCombinedLog}
229}
230
231// LoggingHandler return a http.Handler that wraps h and logs requests to out in
232// Apache Common Log Format (CLF).
233//
234// See http://httpd.apache.org/docs/2.2/logs.html#common for a description of this format.
235//
236// LoggingHandler always sets the ident field of the log to -
237//
238// Example:
239//
240//  r := mux.NewRouter()
241//  r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
242//  	w.Write([]byte("This is a catch-all route"))
243//  })
244//  loggedRouter := handlers.LoggingHandler(os.Stdout, r)
245//  http.ListenAndServe(":1123", loggedRouter)
246//
247func LoggingHandler(out io.Writer, h http.Handler) http.Handler {
248	return loggingHandler{out, h, writeLog}
249}
250
251// CustomLoggingHandler provides a way to supply a custom log formatter
252// while taking advantage of the mechanisms in this package
253func CustomLoggingHandler(out io.Writer, h http.Handler, f LogFormatter) http.Handler {
254	return loggingHandler{out, h, f}
255}
256