filebrowser/http/request_log.go

275 lines
6.7 KiB
Go

package http
import (
"fmt"
"log"
"log/syslog"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/users"
"github.com/tomasen/realip"
)
type LogWriter struct {
Type string
Uri string
syslogWriter *syslog.Writer
fileWriter *os.File
}
var globalLogWriter *LogWriter = nil
func _syslogConnect(type_ string, host string) *syslog.Writer {
writer, err := syslog.Dial(type_, host, syslog.LOG_EMERG|syslog.LOG_SYSLOG, "filebrowser")
if err != nil {
log.Printf("ERROR: fail to connect to the syslog-%s server: %s", type_, host)
log.Println(err)
return nil
} else {
return writer
}
}
func (w *LogWriter) Connect() bool {
if w.Type == "system" {
return true
}
if w.Type == "file" && w.fileWriter != nil {
return true
}
if (w.Type == "syslog-tcp" || w.Type == "syslog-udp") && w.syslogWriter != nil {
return true
}
if w.Type == "file" {
w.fileWriter = _openFile(w.Uri)
} else if w.Type == "syslog-udp" {
w.syslogWriter = _syslogConnect("udp", w.Uri)
} else if w.Type == "syslog-tcp" {
w.syslogWriter = _syslogConnect("tcp", w.Uri)
}
return w.syslogWriter != nil || w.fileWriter != nil
}
func _openFile(path string) *os.File {
d := filepath.Dir(path)
err := os.MkdirAll(d, 0644)
if err != nil {
log.Println("ERROR: fail to create dir " + d)
return nil
}
file, err := os.OpenFile(path, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644)
if err != nil {
log.Println("ERROR: fail to open file " + path)
log.Println(err)
return nil
}
return file
}
func (w *LogWriter) Write(msg string) {
if w.Type == "system" {
log.Println(msg)
} else if w.Type == "file" {
if w.Connect() {
w.fileWriter.Write([]byte(msg + "\n"))
}
} else if w.Type == "syslog-udp" || w.Type == "syslog-tcp" {
if w.Connect() {
w.syslogWriter.Emerg(msg)
}
} else {
log.Println("Unsupported request log output type " + w.Type)
}
}
type RequestLog struct {
user *users.User
ip string
time time.Time
request_size uint64
response_size uint64
path string
method string
status int
elapsed float64
referer string
origin string
}
func (r *RequestLog) user_name() string {
if r.user != nil {
return r.user.Username
}
return "-"
}
func (r *RequestLog) user_id() string {
if r.user != nil {
return fmt.Sprintf("%d", r.user.ID)
}
return "-"
}
func (r *RequestLog) user_scope() string {
if r.user != nil {
if r.user.Scope == "" {
return "."
}
return r.user.Scope
}
return "-"
}
func (r *RequestLog) time_string() string {
return r.time.Format(time.RFC3339)
}
// support placeholders:
//
// %{user_name}
// %{user_id}
// %{user_scope}
// %{ip}
// %{time}
// %{request_size}
// %{response_size}: 0 for Transfer-Encoding=chunked
// %{path}
// %{method}
// %{status}
// %{elapsed}
// %{referer}
// %{origin}
func formatLog(format string, log RequestLog) string {
format = strings.ReplaceAll(format, "%{user_name}", log.user_name())
format = strings.ReplaceAll(format, "%{user_id}", log.user_id())
format = strings.ReplaceAll(format, "%{user_scope}", log.user_scope())
format = strings.ReplaceAll(format, "%{ip}", log.ip)
format = strings.ReplaceAll(format, "%{time}", log.time_string())
format = strings.ReplaceAll(format, "%{request_size}", int2string(log.request_size))
format = strings.ReplaceAll(format, "%{response_size}", int2string(log.response_size))
format = strings.ReplaceAll(format, "%{path}", log.path)
format = strings.ReplaceAll(format, "%{method}", log.method)
format = strings.ReplaceAll(format, "%{status}", int2string(log.status))
format = strings.ReplaceAll(format, "%{elapsed}", float2string(log.elapsed))
format = strings.ReplaceAll(format, "%{referer}", log.referer)
format = strings.ReplaceAll(format, "%{origin}", log.origin)
return format
}
func str2uint64(d string) uint64 {
val, err := strconv.ParseInt(d, 10, 64)
if err != nil {
return 0
}
return uint64(val)
}
func int2string(val any) string {
return fmt.Sprintf("%d", val)
}
func float2string(val float64) string {
return fmt.Sprintf("%f", val)
}
func getRequestSize(r *http.Request) uint64 {
return uint64(r.ContentLength)
}
type myHandler struct {
f http.HandlerFunc
}
func (h *myHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.f(w, r)
}
func _logToOutput(msg string, writer *LogWriter) {
if writer != nil {
writer.Write(msg)
}
}
func _getLogWriter(output string) *LogWriter {
if output == "system" {
return &LogWriter{Type: "system"}
} else if strings.HasPrefix(output, "udp://") {
return &LogWriter{Type: "syslog-udp", Uri: output[6:]}
} else if strings.HasPrefix(output, "tcp://") {
return &LogWriter{Type: "syslog-tcp", Uri: output[6:]}
} else if strings.HasPrefix(output, "file://") {
return &LogWriter{Type: "file", Uri: output[7:]}
}
log.Fatal("Unsupported log output: " + output)
return &LogWriter{}
}
func _globalLogWriter(output string) *LogWriter {
if globalLogWriter == nil {
globalLogWriter = _getLogWriter(output)
}
return globalLogWriter
}
func _log(writer *ResponseWriterWrapper, r *http.Request, user *users.User, server *settings.Server) {
log_ := RequestLog{
user: user,
status: writer.GetStatus(),
elapsed: writer.GetElapsed(),
response_size: writer.GetSize(),
ip: realip.FromRequest(r),
time: writer.GetTime(),
request_size: getRequestSize(r),
origin: r.Header.Get("Origin"),
referer: r.Header.Get("Referer"),
path: r.RequestURI,
method: r.Method,
}
if log_.status == 0 {
log_.status = 200
}
_globalLogWriter(server.RequestLogOutput).Write(formatLog(server.RequestLogFormat, log_))
}
func RequestLogHandlerFunc(handler http.HandlerFunc, server *settings.Server) http.HandlerFunc {
if !server.EnableRequestLog {
return handler
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
writer := MakeResponseWriterWrapper(w)
handler(writer, r)
_log(writer, r, nil, server)
})
}
func RequestLogHandler(handler http.Handler, server *settings.Server) http.Handler {
if !server.EnableRequestLog {
return handler
}
return &myHandler{f: func(w http.ResponseWriter, r *http.Request) {
writer := MakeResponseWriterWrapper(w)
handler.ServeHTTP(writer, r)
_log(writer, r, nil, server)
}}
}
func RequestLogHandleFunc(handle handleFunc, server *settings.Server) handleFunc {
if !server.EnableRequestLog {
return handle
}
return func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
writer := MakeResponseWriterWrapper(w)
status, err := handle(writer, r, d)
writer.status.set(status)
_log(writer, r, d.user, server)
return status, err
}
}