diff --git a/logging.go b/logging.go index d210783..3af6ab9 100644 --- a/logging.go +++ b/logging.go @@ -10,6 +10,7 @@ import ( "net/http" "net/url" "strconv" + "strings" "time" "unicode/utf8" @@ -148,7 +149,7 @@ func appendQuoted(buf []byte, s string) []byte { // buildCommonLogLine builds a log entry for req in Apache Common Log Format. // ts is the timestamp with which the entry should be logged. // status and size are used to provide the response HTTP status and size. -func buildCommonLogLine(req *http.Request, url url.URL, ts time.Time, status int, size int) []byte { +func buildCommonLogLine(req *http.Request, url url.URL, ts time.Time, status int, size int, vhost bool) []byte { username := "-" if url.User != nil { if name := url.User.Username(); name != "" { @@ -163,6 +164,18 @@ func buildCommonLogLine(req *http.Request, url url.URL, ts time.Time, status int uri := req.RequestURI + virtualHost := "" + if vhost { + virtualHost = "-" + if req.Method != http.MethodConnect && !strings.Contains(req.Host, ":") { + a, ok := req.Context().Value(http.LocalAddrContextKey).(net.Addr) + if ok { + s := a.String() + virtualHost = req.Host + s[strings.LastIndex(s, ":"):] + } + } + } + // Requests using the CONNECT method over HTTP/2.0 must use // the authority field (aka r.Host) to identify the target. // Refer: https://httpwg.github.io/specs/rfc7540.html#CONNECT @@ -173,7 +186,11 @@ func buildCommonLogLine(req *http.Request, url url.URL, ts time.Time, status int uri = url.RequestURI() } - buf := make([]byte, 0, 3*(len(host)+len(username)+len(req.Method)+len(uri)+len(req.Proto)+50)/2) + buf := make([]byte, 0, 3*(len(virtualHost)+len(host)+len(username)+len(req.Method)+len(uri)+len(req.Proto)+50)/2) + if vhost { + buf = append(buf, virtualHost...) + buf = append(buf, ' ') + } buf = append(buf, host...) buf = append(buf, " - "...) buf = append(buf, username...) @@ -196,7 +213,7 @@ func buildCommonLogLine(req *http.Request, url url.URL, ts time.Time, status int // ts is the timestamp with which the entry should be logged. // status and size are used to provide the response HTTP status and size. func writeLog(writer io.Writer, params LogFormatterParams) { - buf := buildCommonLogLine(params.Request, params.URL, params.TimeStamp, params.StatusCode, params.Size) + buf := buildCommonLogLine(params.Request, params.URL, params.TimeStamp, params.StatusCode, params.Size, false) buf = append(buf, '\n') _, _ = writer.Write(buf) } @@ -205,7 +222,19 @@ func writeLog(writer io.Writer, params LogFormatterParams) { // ts is the timestamp with which the entry should be logged. // status and size are used to provide the response HTTP status and size. func writeCombinedLog(writer io.Writer, params LogFormatterParams) { - buf := buildCommonLogLine(params.Request, params.URL, params.TimeStamp, params.StatusCode, params.Size) + buf := buildCommonLogLine(params.Request, params.URL, params.TimeStamp, params.StatusCode, params.Size, false) + buf = append(buf, ` "`...) + buf = appendQuoted(buf, params.Request.Referer()) + buf = append(buf, `" "`...) + buf = appendQuoted(buf, params.Request.UserAgent()) + buf = append(buf, '"', '\n') + _, _ = writer.Write(buf) +} + +// writeVhostCombinedLog writes a log entry for req to w in Apache Combined Log Format +// with VirtualHost. +func writeVhostCombinedLog(writer io.Writer, params LogFormatterParams) { + buf := buildCommonLogLine(params.Request, params.URL, params.TimeStamp, params.StatusCode, params.Size, true) buf = append(buf, ` "`...) buf = appendQuoted(buf, params.Request.Referer()) buf = append(buf, `" "`...) @@ -224,6 +253,16 @@ func CombinedLoggingHandler(out io.Writer, h http.Handler) http.Handler { return loggingHandler{out, h, writeCombinedLog} } +// VhostCombinedVLoggingHandler return a http.Handler that wraps h and logs requests to out in +// Apache Combined Log Format with VirtualHost. +// +// See http://httpd.apache.org/docs/2.2/logs.html#combined for a description of this format. +// +// LoggingHandler always sets the ident field of the log to -. +func VhostCombinedLoggingHandler(out io.Writer, h http.Handler) http.Handler { + return loggingHandler{out, h, writeVhostCombinedLog} +} + // LoggingHandler return a http.Handler that wraps h and logs requests to out in // Apache Common Log Format (CLF). // diff --git a/logging_test.go b/logging_test.go index d33847a..6727152 100644 --- a/logging_test.go +++ b/logging_test.go @@ -6,12 +6,14 @@ package handlers import ( "bytes" + "context" "crypto/rand" "encoding/base64" "errors" "fmt" "io/fs" "mime/multipart" + "net" "net/http" "net/http/httptest" "net/url" @@ -229,6 +231,30 @@ func TestLogFormatterCombinedLog_Scenario5(t *testing.T) { LoggingScenario5(t, formatter, expected) } +func TestLogFormatterVhostCombinedLog_Scenario1(t *testing.T) { + formatter := writeVhostCombinedLog + expected := "- 192.168.100.5 - - [26/May/1983:03:30:45 +0200] \"GET / HTTP/1.1\" 200 100 \"http://example.com\" " + + "\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) " + + "AppleWebKit/537.33 (KHTML, like Gecko) Chrome/27.0.1430.0 Safari/537.33\"\n" + LoggingScenario1(t, formatter, expected) +} + +func TestLogFormatterVhostCombinedLog_Scenario2(t *testing.T) { + formatter := writeVhostCombinedLog + expected := "- 192.168.100.5 - - [26/May/1983:03:30:45 +0200] \"CONNECT www.example.com:443 HTTP/2.0\" 200 100 \"http://example.com\" " + + "\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) " + + "AppleWebKit/537.33 (KHTML, like Gecko) Chrome/27.0.1430.0 Safari/537.33\"\n" + LoggingScenario2(t, formatter, expected) +} + +func TestLogFormatterVhostCombinedLog_Scenario3(t *testing.T) { + formatter := writeVhostCombinedLog + expected := "example.com:8080 192.168.100.5 - kamil [26/May/1983:03:30:45 +0200] \"GET / HTTP/1.1\" 401 500 \"http://example.com\" " + + "\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) " + + "AppleWebKit/537.33 (KHTML, like Gecko) Chrome/27.0.1430.0 Safari/537.33\"\n" + LoggingScenario3(t, formatter, expected) +} + func LoggingScenario1(t *testing.T, formatter LogFormatter, expected string) { loc, err := time.LoadLocation("Europe/Warsaw") if err != nil { @@ -265,6 +291,7 @@ func LoggingScenario2(t *testing.T, formatter LogFormatter, expected string) { // CONNECT request over http/2.0 req := constructConnectRequest() + req = req.WithContext(constructVhostAddrCtx("10.0.0.1", 8080)) buf := new(bytes.Buffer) params := LogFormatterParams{ @@ -292,6 +319,7 @@ func LoggingScenario3(t *testing.T, formatter LogFormatter, expected string) { // Request with an unauthorized user req := constructTypicalRequestOk() req.URL.User = url.User("kamil") + req = req.WithContext(constructVhostAddrCtx("10.0.0.1", 8080)) buf := new(bytes.Buffer) params := LogFormatterParams{ @@ -401,3 +429,11 @@ func constructEncodedRequest() *http.Request { req.URL, _ = url.Parse("http://example.com/test?abc=hello%20world&a=b%3F") return req } + +func constructVhostAddrCtx(addr string, port int) context.Context { + ip := net.ParseIP(addr) + + ctx := context.Background() + ctx = context.WithValue(ctx, http.LocalAddrContextKey, &net.TCPAddr{IP: ip, Port: port}) + return ctx +}