log.go (view raw)
1package main
2
3import (
4 "github.com/gorilla/handlers"
5 "io"
6 "net"
7 "net/http"
8 "net/url"
9 "strconv"
10 "time"
11 "unicode/utf8"
12)
13
14// Copy pasted from gorilla handler library, modified slightly
15
16const lowerhex = "0123456789abcdef"
17
18func logFormatter(writer io.Writer, params handlers.LogFormatterParams) {
19 buf := buildCommonLogLine(params.Request, params.URL, params.TimeStamp, params.StatusCode, params.Size)
20 buf = append(buf, '\n')
21 writer.Write(buf)
22}
23
24// buildCommonLogLine builds a log entry for req in Apache Common Log Format.
25// ts is the timestamp with which the entry should be logged.
26// status and size are used to provide the response HTTP status and size.
27func buildCommonLogLine(req *http.Request, url url.URL, ts time.Time, status int, size int) []byte {
28 user := newGetAuthUser(req)
29 username := "-"
30 if user.Username != "" {
31 username = user.Username
32 }
33
34 // Get forwarded IP address
35 ipAddr := req.Header.Get("X-Real-IP")
36 if ipAddr == "" {
37 ipAddr = req.RemoteAddr
38 }
39
40 host, _, err := net.SplitHostPort(ipAddr)
41 if err != nil {
42 host = ipAddr
43 }
44
45 uri := req.RequestURI
46
47 // Requests using the CONNECT method over HTTP/2.0 must use
48 // the authority field (aka r.Host) to identify the target.
49 // Refer: https://httpwg.github.io/specs/rfc7540.html#CONNECT
50 if req.ProtoMajor == 2 && req.Method == "CONNECT" {
51 uri = req.Host
52 }
53 if uri == "" {
54 uri = url.RequestURI()
55 }
56
57 desthost := req.Host
58
59 buf := make([]byte, 0, 3*(len(host)+len(desthost)+len(username)+len(req.Method)+len(uri)+len(req.Proto)+50)/2)
60 buf = append(buf, host...)
61 buf = append(buf, " - "...)
62 buf = append(buf, username...)
63 buf = append(buf, " ["...)
64 buf = append(buf, ts.Format("02/Jan/2006:15:04:05 -0700")...)
65 buf = append(buf, `] `...)
66 buf = append(buf, desthost...)
67 buf = append(buf, ` "`...)
68 buf = append(buf, req.Method...)
69 buf = append(buf, " "...)
70 buf = appendQuoted(buf, uri)
71 buf = append(buf, " "...)
72 buf = append(buf, req.Proto...)
73 buf = append(buf, `" `...)
74 buf = append(buf, strconv.Itoa(status)...)
75 buf = append(buf, " "...)
76 buf = append(buf, strconv.Itoa(size)...)
77 return buf
78}
79
80func appendQuoted(buf []byte, s string) []byte {
81 var runeTmp [utf8.UTFMax]byte
82 for width := 0; len(s) > 0; s = s[width:] {
83 r := rune(s[0])
84 width = 1
85 if r >= utf8.RuneSelf {
86 r, width = utf8.DecodeRuneInString(s)
87 }
88 if width == 1 && r == utf8.RuneError {
89 buf = append(buf, `\x`...)
90 buf = append(buf, lowerhex[s[0]>>4])
91 buf = append(buf, lowerhex[s[0]&0xF])
92 continue
93 }
94 if r == rune('"') || r == '\\' { // always backslashed
95 buf = append(buf, '\\')
96 buf = append(buf, byte(r))
97 continue
98 }
99 if strconv.IsPrint(r) {
100 n := utf8.EncodeRune(runeTmp[:], r)
101 buf = append(buf, runeTmp[:n]...)
102 continue
103 }
104 switch r {
105 case '\a':
106 buf = append(buf, `\a`...)
107 case '\b':
108 buf = append(buf, `\b`...)
109 case '\f':
110 buf = append(buf, `\f`...)
111 case '\n':
112 buf = append(buf, `\n`...)
113 case '\r':
114 buf = append(buf, `\r`...)
115 case '\t':
116 buf = append(buf, `\t`...)
117 case '\v':
118 buf = append(buf, `\v`...)
119 default:
120 switch {
121 case r < ' ':
122 buf = append(buf, `\x`...)
123 buf = append(buf, lowerhex[s[0]>>4])
124 buf = append(buf, lowerhex[s[0]&0xF])
125 case r > utf8.MaxRune:
126 r = 0xFFFD
127 fallthrough
128 case r < 0x10000:
129 buf = append(buf, `\u`...)
130 for s := 12; s >= 0; s -= 4 {
131 buf = append(buf, lowerhex[r>>uint(s)&0xF])
132 }
133 default:
134 buf = append(buf, `\U`...)
135 for s := 28; s >= 0; s -= 4 {
136 buf = append(buf, lowerhex[r>>uint(s)&0xF])
137 }
138 }
139 }
140 }
141 return buf
142}
143
144// Parse logs and write to database
145
146// Anonymize user and IP?