From c85193ce0c227ad9c0be2d9a04bc97714c29d7b5 Mon Sep 17 00:00:00 2001 From: wwt Date: Thu, 21 Dec 2023 15:05:15 +0800 Subject: [PATCH] feat: request logging --- cmd/root.go | 8 +++ http/data.go | 15 ++++- http/request_log.go | 129 +++++++++++++++++++++++++++++++++++++++++++ settings/settings.go | 2 + 4 files changed, 152 insertions(+), 2 deletions(-) create mode 100644 http/request_log.go diff --git a/cmd/root.go b/cmd/root.go index 7ec4d441..77c3f6b4 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -70,6 +70,8 @@ func addServerFlags(flags *pflag.FlagSet) { flags.Bool("disable-preview-resize", false, "disable resize of image previews") flags.Bool("disable-exec", false, "disables Command Runner feature") flags.Bool("disable-type-detection-by-header", false, "disables type detection by reading file headers") + flags.Bool("enable-request-log", false, "enable logging all the request") + flags.String("request-log-format", "%{user_name} %{ip} %{method} %{path} %{response_size}", "logging format for the request") } var rootCmd = &cobra.Command{ @@ -262,6 +264,12 @@ func getRunParams(flags *pflag.FlagSet, st *storage.Storage) *settings.Server { _, disableExec := getParamB(flags, "disable-exec") server.EnableExec = !disableExec + _, enableRequestLog := getParamB(flags, "enable-request-log") + server.EnableRequestLog = enableRequestLog + + requestLogFormat := getParam(flags, "request-log-format") + server.RequestLogFormat = requestLogFormat + if val, set := getParamB(flags, "token-expiration-time"); set { server.TokenExpirationTime = val } diff --git a/http/data.go b/http/data.go index 5ba87313..e480de43 100644 --- a/http/data.go +++ b/http/data.go @@ -4,6 +4,7 @@ import ( "log" "net/http" "strconv" + "time" "github.com/tomasen/realip" @@ -49,6 +50,7 @@ func (d *data) Check(path string) bool { func handle(fn handleFunc, prefix string, store *storage.Storage, server *settings.Server) http.Handler { handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + begin := time.Now() for k, v := range globalHeaders { w.Header().Set(k, v) } @@ -59,12 +61,21 @@ func handle(fn handleFunc, prefix string, store *storage.Storage, server *settin return } - status, err := fn(w, r, &data{ + d := data{ Runner: &runner.Runner{Enabled: server.EnableExec, Settings: settings}, store: store, settings: settings, server: server, - }) + } + + status, err := fn(w, r, &d) + if server.EnableRequestLog { + LogRequest(w, r, server.RequestLogFormat, RequestLog{ + user: d.user, + status: status, + elapsed: time.Now().Sub(begin).Seconds(), + }) + } if status >= 400 || err != nil { clientIP := realip.FromRequest(r) diff --git a/http/request_log.go b/http/request_log.go new file mode 100644 index 00000000..5ce0be16 --- /dev/null +++ b/http/request_log.go @@ -0,0 +1,129 @@ +package http + +import ( + "fmt" + "log" + "net/http" + "strconv" + "strings" + "time" + + "github.com/filebrowser/filebrowser/v2/users" +) + +type RequestLog struct { + user *users.User + ip string + time time.Time + request_size int64 + response_size int64 + path string + method string + status int + elapsed float64 +} + +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 { + return r.user.Scope + } + return "." +} + +func (r *RequestLog) time_string() string { + return r.time.Format(time.RFC3339) +} + +func LogRequest(w http.ResponseWriter, r *http.Request, format string, log_ RequestLog) { + if log_.status == 0 { + log_.status = 200 + } + log_.ip = getRealIp(r) + log_.time = time.Now() + log_.request_size = r.ContentLength + if log_.response_size == 0 { + log_.response_size = parseSize(w.Header().Get("Content-Length")) + } + log_.path = r.URL.Path + log_.method = r.Method + log.Println(formatLog(format, log_)) +} + +// support placeholders: +// +// %{user_name} +// %{user_id} +// %{user_scope} +// %{ip} +// %{time} +// %{request_size} +// %{response_size}: 0 for Transfer-Encoding=chunked +// %{path} +// %{method} +// %{status} +// %{elapsed} +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)) + return format +} + +func getRealIp(r *http.Request) string { + remoteAddr := r.RemoteAddr + forwardedFor := parseFirstItem(r.Header.Get("X-Forwarded-For")) + realIp := parseFirstItem(r.Header.Get("X-Real-IP")) + if forwardedFor != "" { + return forwardedFor + } + if realIp != "" { + return realIp + } + return remoteAddr +} + +func parseFirstItem(s string) string { + items := strings.Split(s, ",") + if len(items) == 0 { + return "" + } + return items[0] +} + +func parseSize(d string) int64 { + val, err := strconv.ParseInt(d, 10, 64) + if err != nil { + return 0 + } + return val +} + +func int2string(val any) string { + return fmt.Sprintf("%d", val) +} + +func float2string(val float64) string { + return fmt.Sprintf("%f", val) +} diff --git a/settings/settings.go b/settings/settings.go index 4e62159b..aa088751 100644 --- a/settings/settings.go +++ b/settings/settings.go @@ -50,6 +50,8 @@ type Server struct { TypeDetectionByHeader bool `json:"typeDetectionByHeader"` AuthHook string `json:"authHook"` TokenExpirationTime string `json:"tokenExpirationTime"` + EnableRequestLog bool `json:"enableRequestLog"` + RequestLogFormat string `json:"requestLogFormat"` } // Clean cleans any variables that might need cleaning.