diff --git a/cmd/generic-handlers.go b/cmd/generic-handlers.go index e4bfb0ef8..2a731a95a 100644 --- a/cmd/generic-handlers.go +++ b/cmd/generic-handlers.go @@ -549,7 +549,7 @@ func addCustomHeaders(h http.Handler) http.Handler { // part of the log entry, Error response XML and auditing. // Set custom headers such as x-amz-request-id for each request. w.Header().Set(xhttp.AmzRequestID, mustGetRequestID(UTCNow())) - h.ServeHTTP(logger.NewResponseWriter(w), r) + h.ServeHTTP(xhttp.NewResponseRecorder(w), r) }) } diff --git a/cmd/handler-utils.go b/cmd/handler-utils.go index 1cc70b773..63facc700 100644 --- a/cmd/handler-utils.go +++ b/cmd/handler-utils.go @@ -350,7 +350,7 @@ func collectAPIStats(api string, f http.HandlerFunc) http.HandlerFunc { globalHTTPStats.currentS3Requests.Inc(api) defer globalHTTPStats.currentS3Requests.Dec(api) - statsWriter := logger.NewResponseWriter(w) + statsWriter := xhttp.NewResponseRecorder(w) f.ServeHTTP(statsWriter, r) diff --git a/cmd/http-stats.go b/cmd/http-stats.go index 5e00231fe..07bb1fc71 100644 --- a/cmd/http-stats.go +++ b/cmd/http-stats.go @@ -23,7 +23,7 @@ import ( "sync" "sync/atomic" - "github.com/minio/minio/internal/logger" + xhttp "github.com/minio/minio/internal/http" "github.com/prometheus/client_golang/prometheus" ) @@ -295,7 +295,7 @@ func (st *HTTPStats) toServerHTTPStats() ServerHTTPStats { } // Update statistics from http request and response data -func (st *HTTPStats) updateStats(api string, r *http.Request, w *logger.ResponseWriter) { +func (st *HTTPStats) updateStats(api string, r *http.Request, w *xhttp.ResponseRecorder) { // Ignore non S3 requests if strings.HasSuffix(r.URL.Path, minioReservedBucketPathWithSlash) { return diff --git a/cmd/http-tracer.go b/cmd/http-tracer.go index 05fa725c4..3ec33382b 100644 --- a/cmd/http-tracer.go +++ b/cmd/http-tracer.go @@ -18,9 +18,7 @@ package cmd import ( - "bytes" "context" - "io" "net" "net/http" "reflect" @@ -32,54 +30,9 @@ import ( "github.com/minio/madmin-go" "github.com/minio/minio/internal/handlers" - "github.com/minio/minio/internal/logger" + xhttp "github.com/minio/minio/internal/http" ) -// recordRequest - records the first recLen bytes -// of a given io.Reader -type recordRequest struct { - // Data source to record - io.Reader - // Response body should be logged - logBody bool - // Internal recording buffer - buf bytes.Buffer - // total bytes read including header size - bytesRead int -} - -func (r *recordRequest) Close() error { - // no-op - return nil -} - -func (r *recordRequest) Read(p []byte) (n int, err error) { - n, err = r.Reader.Read(p) - r.bytesRead += n - - if r.logBody { - r.buf.Write(p[:n]) - } - if err != nil { - return n, err - } - return n, err -} - -func (r *recordRequest) BodySize() int { - return r.bytesRead -} - -// Return the bytes that were recorded. -func (r *recordRequest) Data() []byte { - // If body logging is enabled then we return the actual body - if r.logBody { - return r.buf.Bytes() - } - // ... otherwise we return placeholder - return logger.BodyPlaceHolder -} - var ldapPwdRegex = regexp.MustCompile("(^.*?)LDAPPassword=([^&]*?)(&(.*?))?$") // redact LDAP password if part of string @@ -116,8 +69,8 @@ const contextTraceReqKey = contextTraceReqType("request-trace-info") // Hold related tracing data of a http request, any handler // can modify this struct to modify the trace information . type traceCtxt struct { - requestRecorder *recordRequest - responseRecorder *logger.ResponseWriter + requestRecorder *xhttp.RequestRecorder + responseRecorder *xhttp.ResponseRecorder funcName string } @@ -136,8 +89,8 @@ func httpTracer(h http.Handler) http.Handler { r = r.WithContext(ctx) // Setup a http request and response body recorder - reqRecorder := &recordRequest{Reader: r.Body} - respRecorder := logger.NewResponseWriter(w) + reqRecorder := &xhttp.RequestRecorder{Reader: r.Body} + respRecorder := xhttp.NewResponseRecorder(w) tc.requestRecorder = reqRecorder tc.responseRecorder = respRecorder @@ -240,7 +193,7 @@ func httpTrace(f http.HandlerFunc, logBody bool) http.HandlerFunc { } tc.funcName = getOpName(runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name()) - tc.requestRecorder.logBody = logBody + tc.requestRecorder.LogBody = logBody tc.responseRecorder.LogAllBody = logBody tc.responseRecorder.LogErrBody = true diff --git a/cmd/object-handlers.go b/cmd/object-handlers.go index 37ce3c6a3..c3b3d236a 100644 --- a/cmd/object-handlers.go +++ b/cmd/object-handlers.go @@ -3542,7 +3542,7 @@ func (api objectAPIHandlers) PostRestoreObjectHandler(w http.ResponseWriter, r * return } nr := httptest.NewRecorder() - rw := logger.NewResponseWriter(nr) + rw := xhttp.NewResponseRecorder(nr) rw.LogErrBody = true rw.LogAllBody = true rreq.SelectParameters.Evaluate(rw) diff --git a/internal/http/request-recorder.go b/internal/http/request-recorder.go new file mode 100644 index 000000000..c098624a5 --- /dev/null +++ b/internal/http/request-recorder.go @@ -0,0 +1,72 @@ +// Copyright (c) 2015-2022 MinIO, Inc. +// +// This file is part of MinIO Object Storage stack +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package http + +import ( + "bytes" + "io" +) + +// RequestRecorder - records the +// of a given io.Reader +type RequestRecorder struct { + // Data source to record + io.Reader + // Response body should be logged + LogBody bool + + // internal recording buffer + buf bytes.Buffer + // total bytes read including header size + bytesRead int +} + +// Close is a no operation closer +func (r *RequestRecorder) Close() error { + // no-op + return nil +} + +// Read reads from the internal reader and counts/save the body in the memory +func (r *RequestRecorder) Read(p []byte) (n int, err error) { + n, err = r.Reader.Read(p) + r.bytesRead += n + + if r.LogBody { + r.buf.Write(p[:n]) + } + if err != nil { + return n, err + } + return n, err +} + +// BodySize returns the body size if the currently read object +func (r *RequestRecorder) BodySize() int { + return r.bytesRead +} + +// Data returns the bytes that were recorded. +func (r *RequestRecorder) Data() []byte { + // If body logging is enabled then we return the actual body + if r.LogBody { + return r.buf.Bytes() + } + // ... otherwise we return placeholder + return BodyPlaceHolder +} diff --git a/internal/http/response-recorder.go b/internal/http/response-recorder.go new file mode 100644 index 000000000..c90f86828 --- /dev/null +++ b/internal/http/response-recorder.go @@ -0,0 +1,129 @@ +// Copyright (c) 2015-2022 MinIO, Inc. +// +// This file is part of MinIO Object Storage stack +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package http + +import ( + "bytes" + "fmt" + "io" + "net/http" + "time" +) + +// ResponseRecorder - is a wrapper to trap the http response +// status code and to record the response body +type ResponseRecorder struct { + http.ResponseWriter + StatusCode int + // Log body of 4xx or 5xx responses + LogErrBody bool + // Log body of all responses + LogAllBody bool + + TimeToFirstByte time.Duration + StartTime time.Time + // number of bytes written + bytesWritten int + // number of bytes of response headers written + headerBytesWritten int + // Internal recording buffer + headers bytes.Buffer + body bytes.Buffer + // Indicate if headers are written in the log + headersLogged bool +} + +// NewResponseRecorder - returns a wrapped response writer to trap +// http status codes for auditing purposes. +func NewResponseRecorder(w http.ResponseWriter) *ResponseRecorder { + return &ResponseRecorder{ + ResponseWriter: w, + StatusCode: http.StatusOK, + StartTime: time.Now().UTC(), + } +} + +func (lrw *ResponseRecorder) Write(p []byte) (int, error) { + if !lrw.headersLogged { + // We assume the response code to be '200 OK' when WriteHeader() is not called, + // that way following Golang HTTP response behavior. + lrw.WriteHeader(http.StatusOK) + } + n, err := lrw.ResponseWriter.Write(p) + lrw.bytesWritten += n + if lrw.TimeToFirstByte == 0 { + lrw.TimeToFirstByte = time.Now().UTC().Sub(lrw.StartTime) + } + if (lrw.LogErrBody && lrw.StatusCode >= http.StatusBadRequest) || lrw.LogAllBody { + // Always logging error responses. + lrw.body.Write(p) + } + if err != nil { + return n, err + } + return n, err +} + +// Write the headers into the given buffer +func (lrw *ResponseRecorder) writeHeaders(w io.Writer, statusCode int, headers http.Header) { + n, _ := fmt.Fprintf(w, "%d %s\n", statusCode, http.StatusText(statusCode)) + lrw.headerBytesWritten += n + for k, v := range headers { + n, _ := fmt.Fprintf(w, "%s: %s\n", k, v[0]) + lrw.headerBytesWritten += n + } +} + +// BodyPlaceHolder returns a dummy body placeholder +var BodyPlaceHolder = []byte("") + +// Body - Return response body. +func (lrw *ResponseRecorder) Body() []byte { + // If there was an error response or body logging is enabled + // then we return the body contents + if (lrw.LogErrBody && lrw.StatusCode >= http.StatusBadRequest) || lrw.LogAllBody { + return lrw.body.Bytes() + } + // ... otherwise we return the place holder + return BodyPlaceHolder +} + +// WriteHeader - writes http status code +func (lrw *ResponseRecorder) WriteHeader(code int) { + if !lrw.headersLogged { + lrw.StatusCode = code + lrw.writeHeaders(&lrw.headers, code, lrw.ResponseWriter.Header()) + lrw.headersLogged = true + lrw.ResponseWriter.WriteHeader(code) + } +} + +// Flush - Calls the underlying Flush. +func (lrw *ResponseRecorder) Flush() { + lrw.ResponseWriter.(http.Flusher).Flush() +} + +// Size - returns the number of bytes written +func (lrw *ResponseRecorder) Size() int { + return lrw.bytesWritten +} + +// HeaderSize - returns the number of bytes of response headers written +func (lrw *ResponseRecorder) HeaderSize() int { + return lrw.headerBytesWritten +} diff --git a/internal/logger/audit.go b/internal/logger/audit.go index f0a8b0c30..e23de5ba1 100644 --- a/internal/logger/audit.go +++ b/internal/logger/audit.go @@ -18,10 +18,8 @@ package logger import ( - "bytes" "context" "fmt" - "io" "net/http" "strconv" "time" @@ -32,108 +30,6 @@ import ( "github.com/minio/minio/internal/logger/message/audit" ) -// ResponseWriter - is a wrapper to trap the http response status code. -type ResponseWriter struct { - http.ResponseWriter - StatusCode int - // Log body of 4xx or 5xx responses - LogErrBody bool - // Log body of all responses - LogAllBody bool - - TimeToFirstByte time.Duration - StartTime time.Time - // number of bytes written - bytesWritten int - // number of bytes of response headers written - headerBytesWritten int - // Internal recording buffer - headers bytes.Buffer - body bytes.Buffer - // Indicate if headers are written in the log - headersLogged bool -} - -// NewResponseWriter - returns a wrapped response writer to trap -// http status codes for auditing purposes. -func NewResponseWriter(w http.ResponseWriter) *ResponseWriter { - return &ResponseWriter{ - ResponseWriter: w, - StatusCode: http.StatusOK, - StartTime: time.Now().UTC(), - } -} - -func (lrw *ResponseWriter) Write(p []byte) (int, error) { - if !lrw.headersLogged { - // We assume the response code to be '200 OK' when WriteHeader() is not called, - // that way following Golang HTTP response behavior. - lrw.WriteHeader(http.StatusOK) - } - n, err := lrw.ResponseWriter.Write(p) - lrw.bytesWritten += n - if lrw.TimeToFirstByte == 0 { - lrw.TimeToFirstByte = time.Now().UTC().Sub(lrw.StartTime) - } - if (lrw.LogErrBody && lrw.StatusCode >= http.StatusBadRequest) || lrw.LogAllBody { - // Always logging error responses. - lrw.body.Write(p) - } - if err != nil { - return n, err - } - return n, err -} - -// Write the headers into the given buffer -func (lrw *ResponseWriter) writeHeaders(w io.Writer, statusCode int, headers http.Header) { - n, _ := fmt.Fprintf(w, "%d %s\n", statusCode, http.StatusText(statusCode)) - lrw.headerBytesWritten += n - for k, v := range headers { - n, _ := fmt.Fprintf(w, "%s: %s\n", k, v[0]) - lrw.headerBytesWritten += n - } -} - -// BodyPlaceHolder returns a dummy body placeholder -var BodyPlaceHolder = []byte("") - -// Body - Return response body. -func (lrw *ResponseWriter) Body() []byte { - // If there was an error response or body logging is enabled - // then we return the body contents - if (lrw.LogErrBody && lrw.StatusCode >= http.StatusBadRequest) || lrw.LogAllBody { - return lrw.body.Bytes() - } - // ... otherwise we return the place holder - return BodyPlaceHolder -} - -// WriteHeader - writes http status code -func (lrw *ResponseWriter) WriteHeader(code int) { - if !lrw.headersLogged { - lrw.StatusCode = code - lrw.writeHeaders(&lrw.headers, code, lrw.ResponseWriter.Header()) - lrw.headersLogged = true - lrw.ResponseWriter.WriteHeader(code) - } -} - -// Flush - Calls the underlying Flush. -func (lrw *ResponseWriter) Flush() { - lrw.ResponseWriter.(http.Flusher).Flush() -} - -// Size - returns the number of bytes written -func (lrw *ResponseWriter) Size() int { - return lrw.bytesWritten -} - -// HeaderSize - returns the number of bytes of response headers written -func (lrw *ResponseWriter) HeaderSize() int { - return lrw.headerBytesWritten -} - const contextAuditKey = contextKeyType("audit-entry") // SetAuditEntry sets Audit info in the context. @@ -197,13 +93,13 @@ func AuditLog(ctx context.Context, w http.ResponseWriter, r *http.Request, reqCl headerBytes int64 ) - var st *ResponseWriter + var st *xhttp.ResponseRecorder switch v := w.(type) { - case *ResponseWriter: + case *xhttp.ResponseRecorder: st = v case *gzhttp.GzipResponseWriter: // the writer may be obscured by gzip response writer - if rw, ok := v.ResponseWriter.(*ResponseWriter); ok { + if rw, ok := v.ResponseWriter.(*xhttp.ResponseRecorder); ok { st = rw } }