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  }