all repos — flounder @ 422f8d44a496cf7d701c731615c8c6c043f3a9da

A small site builder for the Gemini protocol

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}