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(req.RemoteAddr)
41 if err != nil {
42 host = req.RemoteAddr
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 buf := make([]byte, 0, 3*(len(host)+len(username)+len(req.Method)+len(uri)+len(req.Proto)+50)/2)
58 buf = append(buf, host...)
59 buf = append(buf, " - "...)
60 buf = append(buf, username...)
61 buf = append(buf, " ["...)
62 buf = append(buf, ts.Format("02/Jan/2006:15:04:05 -0700")...)
63 buf = append(buf, `] "`...)
64 buf = append(buf, req.Method...)
65 buf = append(buf, " "...)
66 buf = appendQuoted(buf, uri)
67 buf = append(buf, " "...)
68 buf = append(buf, req.Proto...)
69 buf = append(buf, `" `...)
70 buf = append(buf, strconv.Itoa(status)...)
71 buf = append(buf, " "...)
72 buf = append(buf, strconv.Itoa(size)...)
73 return buf
74}
75
76func appendQuoted(buf []byte, s string) []byte {
77 var runeTmp [utf8.UTFMax]byte
78 for width := 0; len(s) > 0; s = s[width:] {
79 r := rune(s[0])
80 width = 1
81 if r >= utf8.RuneSelf {
82 r, width = utf8.DecodeRuneInString(s)
83 }
84 if width == 1 && r == utf8.RuneError {
85 buf = append(buf, `\x`...)
86 buf = append(buf, lowerhex[s[0]>>4])
87 buf = append(buf, lowerhex[s[0]&0xF])
88 continue
89 }
90 if r == rune('"') || r == '\\' { // always backslashed
91 buf = append(buf, '\\')
92 buf = append(buf, byte(r))
93 continue
94 }
95 if strconv.IsPrint(r) {
96 n := utf8.EncodeRune(runeTmp[:], r)
97 buf = append(buf, runeTmp[:n]...)
98 continue
99 }
100 switch r {
101 case '\a':
102 buf = append(buf, `\a`...)
103 case '\b':
104 buf = append(buf, `\b`...)
105 case '\f':
106 buf = append(buf, `\f`...)
107 case '\n':
108 buf = append(buf, `\n`...)
109 case '\r':
110 buf = append(buf, `\r`...)
111 case '\t':
112 buf = append(buf, `\t`...)
113 case '\v':
114 buf = append(buf, `\v`...)
115 default:
116 switch {
117 case r < ' ':
118 buf = append(buf, `\x`...)
119 buf = append(buf, lowerhex[s[0]>>4])
120 buf = append(buf, lowerhex[s[0]&0xF])
121 case r > utf8.MaxRune:
122 r = 0xFFFD
123 fallthrough
124 case r < 0x10000:
125 buf = append(buf, `\u`...)
126 for s := 12; s >= 0; s -= 4 {
127 buf = append(buf, lowerhex[r>>uint(s)&0xF])
128 }
129 default:
130 buf = append(buf, `\U`...)
131 for s := 28; s >= 0; s -= 4 {
132 buf = append(buf, lowerhex[r>>uint(s)&0xF])
133 }
134 }
135 }
136 }
137 return buf
138}
139
140// Parse logs and write to database
141
142// Anonymize user and IP?