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