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