github.com/minio/minio@v0.0.0-20240328213742-3f72439b8a27/internal/http/response-recorder.go (about) 1 // Copyright (c) 2015-2022 MinIO, Inc. 2 // 3 // This file is part of MinIO Object Storage stack 4 // 5 // This program is free software: you can redistribute it and/or modify 6 // it under the terms of the GNU Affero General Public License as published by 7 // the Free Software Foundation, either version 3 of the License, or 8 // (at your option) any later version. 9 // 10 // This program is distributed in the hope that it will be useful 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 // GNU Affero General Public License for more details. 14 // 15 // You should have received a copy of the GNU Affero General Public License 16 // along with this program. If not, see <http://www.gnu.org/licenses/>. 17 18 package http 19 20 import ( 21 "bufio" 22 "bytes" 23 "errors" 24 "fmt" 25 "io" 26 "net" 27 "net/http" 28 "time" 29 ) 30 31 // ResponseRecorder - is a wrapper to trap the http response 32 // status code and to record the response body 33 type ResponseRecorder struct { 34 http.ResponseWriter 35 io.ReaderFrom 36 StatusCode int 37 // Log body of 4xx or 5xx responses 38 LogErrBody bool 39 // Log body of all responses 40 LogAllBody bool 41 42 TimeToFirstByte time.Duration 43 StartTime time.Time 44 // number of bytes written 45 bytesWritten int 46 // number of bytes of response headers written 47 headerBytesWritten int 48 // Internal recording buffer 49 headers bytes.Buffer 50 body bytes.Buffer 51 // Indicate if headers are written in the log 52 headersLogged bool 53 } 54 55 // Hijack - hijacks the underlying connection 56 func (lrw *ResponseRecorder) Hijack() (net.Conn, *bufio.ReadWriter, error) { 57 hj, ok := lrw.ResponseWriter.(http.Hijacker) 58 if !ok { 59 return nil, nil, fmt.Errorf("response writer does not support hijacking. Type is %T", lrw.ResponseWriter) 60 } 61 return hj.Hijack() 62 } 63 64 // NewResponseRecorder - returns a wrapped response writer to trap 65 // http status codes for auditing purposes. 66 func NewResponseRecorder(w http.ResponseWriter) *ResponseRecorder { 67 rf, _ := w.(io.ReaderFrom) 68 return &ResponseRecorder{ 69 ResponseWriter: w, 70 ReaderFrom: rf, 71 StatusCode: http.StatusOK, 72 StartTime: time.Now().UTC(), 73 } 74 } 75 76 // ErrNotImplemented when a functionality is not implemented 77 var ErrNotImplemented = errors.New("not implemented") 78 79 // ReadFrom implements support for calling internal io.ReaderFrom implementations 80 // returns an error if the underlying ResponseWriter does not implement io.ReaderFrom 81 func (lrw *ResponseRecorder) ReadFrom(r io.Reader) (int64, error) { 82 if lrw.ReaderFrom != nil { 83 n, err := lrw.ReaderFrom.ReadFrom(r) 84 lrw.bytesWritten += int(n) 85 return n, err 86 } 87 return 0, ErrNotImplemented 88 } 89 90 func (lrw *ResponseRecorder) Write(p []byte) (int, error) { 91 if !lrw.headersLogged { 92 // We assume the response code to be '200 OK' when WriteHeader() is not called, 93 // that way following Golang HTTP response behavior. 94 lrw.WriteHeader(http.StatusOK) 95 } 96 n, err := lrw.ResponseWriter.Write(p) 97 lrw.bytesWritten += n 98 if lrw.TimeToFirstByte == 0 { 99 lrw.TimeToFirstByte = time.Now().UTC().Sub(lrw.StartTime) 100 } 101 gzipped := lrw.Header().Get("Content-Encoding") == "gzip" 102 if !gzipped && ((lrw.LogErrBody && lrw.StatusCode >= http.StatusBadRequest) || lrw.LogAllBody) { 103 // Always logging error responses. 104 lrw.body.Write(p) 105 } 106 if err != nil { 107 return n, err 108 } 109 return n, err 110 } 111 112 // Write the headers into the given buffer 113 func (lrw *ResponseRecorder) writeHeaders(w io.Writer, statusCode int, headers http.Header) { 114 n, _ := fmt.Fprintf(w, "%d %s\n", statusCode, http.StatusText(statusCode)) 115 lrw.headerBytesWritten += n 116 for k, v := range headers { 117 n, _ := fmt.Fprintf(w, "%s: %s\n", k, v[0]) 118 lrw.headerBytesWritten += n 119 } 120 } 121 122 // blobBody returns a dummy body placeholder for blob (binary stream) 123 var blobBody = []byte("<BLOB>") 124 125 // gzippedBody returns a dummy body placeholder for gzipped content 126 var gzippedBody = []byte("<GZIP>") 127 128 // Body - Return response body. 129 func (lrw *ResponseRecorder) Body() []byte { 130 if lrw.Header().Get("Content-Encoding") == "gzip" { 131 // ... otherwise we return the <GZIP> place holder 132 return gzippedBody 133 } 134 // If there was an error response or body logging is enabled 135 // then we return the body contents 136 if (lrw.LogErrBody && lrw.StatusCode >= http.StatusBadRequest) || lrw.LogAllBody { 137 return lrw.body.Bytes() 138 } 139 // ... otherwise we return the <BLOB> place holder 140 return blobBody 141 } 142 143 // WriteHeader - writes http status code 144 func (lrw *ResponseRecorder) WriteHeader(code int) { 145 if !lrw.headersLogged { 146 lrw.StatusCode = code 147 lrw.writeHeaders(&lrw.headers, code, lrw.ResponseWriter.Header()) 148 lrw.headersLogged = true 149 lrw.ResponseWriter.WriteHeader(code) 150 } 151 } 152 153 // Flush - Calls the underlying Flush. 154 func (lrw *ResponseRecorder) Flush() { 155 lrw.ResponseWriter.(http.Flusher).Flush() 156 } 157 158 // Size - returns the number of bytes written 159 func (lrw *ResponseRecorder) Size() int { 160 return lrw.bytesWritten 161 } 162 163 // HeaderSize - returns the number of bytes of response headers written 164 func (lrw *ResponseRecorder) HeaderSize() int { 165 return lrw.headerBytesWritten 166 }