github.com/hellobchain/third_party@v0.0.0-20230331131523-deb0478a2e52/go-chi/chi/middleware/compress.go (about)

     1  package middleware
     2  
     3  import (
     4  	"bufio"
     5  	"compress/flate"
     6  	"compress/gzip"
     7  	"errors"
     8  	"io"
     9  	"net"
    10  	"strings"
    11  
    12  	"github.com/hellobchain/newcryptosm/http"
    13  )
    14  
    15  type encoding int
    16  
    17  const (
    18  	encodingNone encoding = iota
    19  	encodingGzip
    20  	encodingDeflate
    21  )
    22  
    23  var defaultContentTypes = map[string]struct{}{
    24  	"text/html":                struct{}{},
    25  	"text/css":                 struct{}{},
    26  	"text/plain":               struct{}{},
    27  	"text/javascript":          struct{}{},
    28  	"application/javascript":   struct{}{},
    29  	"application/x-javascript": struct{}{},
    30  	"application/json":         struct{}{},
    31  	"application/atom+xml":     struct{}{},
    32  	"application/rss+xml":      struct{}{},
    33  }
    34  
    35  // DefaultCompress is a middleware that compresses response
    36  // body of predefined content types to a data format based
    37  // on Accept-Encoding request header. It uses a default
    38  // compression level.
    39  func DefaultCompress(next http.Handler) http.Handler {
    40  	return Compress(flate.DefaultCompression)(next)
    41  }
    42  
    43  // Compress is a middleware that compresses response
    44  // body of a given content types to a data format based
    45  // on Accept-Encoding request header. It uses a given
    46  // compression level.
    47  func Compress(level int, types ...string) func(next http.Handler) http.Handler {
    48  	contentTypes := defaultContentTypes
    49  	if len(types) > 0 {
    50  		contentTypes = make(map[string]struct{}, len(types))
    51  		for _, t := range types {
    52  			contentTypes[t] = struct{}{}
    53  		}
    54  	}
    55  
    56  	return func(next http.Handler) http.Handler {
    57  		fn := func(w http.ResponseWriter, r *http.Request) {
    58  			mcw := &maybeCompressResponseWriter{
    59  				ResponseWriter: w,
    60  				w:              w,
    61  				contentTypes:   contentTypes,
    62  				encoding:       selectEncoding(r.Header),
    63  				level:          level,
    64  			}
    65  			defer mcw.Close()
    66  
    67  			next.ServeHTTP(mcw, r)
    68  		}
    69  
    70  		return http.HandlerFunc(fn)
    71  	}
    72  }
    73  
    74  func selectEncoding(h http.Header) encoding {
    75  	enc := h.Get("Accept-Encoding")
    76  
    77  	switch {
    78  	// TODO:
    79  	// case "br":    // Brotli, experimental. Firefox 2016, to-be-in Chromium.
    80  	// case "lzma":  // Opera.
    81  	// case "sdch":  // Chrome, Android. Gzip output + dictionary header.
    82  
    83  	case strings.Contains(enc, "gzip"):
    84  		// TODO: Exception for old MSIE browsers that can't handle non-HTML?
    85  		// https://zoompf.com/blog/2012/02/lose-the-wait-http-compression
    86  		return encodingGzip
    87  
    88  	case strings.Contains(enc, "deflate"):
    89  		// HTTP 1.1 "deflate" (RFC 2616) stands for DEFLATE data (RFC 1951)
    90  		// wrapped with zlib (RFC 1950). The zlib wrapper uses Adler-32
    91  		// checksum compared to CRC-32 used in "gzip" and thus is faster.
    92  		//
    93  		// But.. some old browsers (MSIE, Safari 5.1) incorrectly expect
    94  		// raw DEFLATE data only, without the mentioned zlib wrapper.
    95  		// Because of this major confusion, most modern browsers try it
    96  		// both ways, first looking for zlib headers.
    97  		// Quote by Mark Adler: http://stackoverflow.com/a/9186091/385548
    98  		//
    99  		// The list of browsers having problems is quite big, see:
   100  		// http://zoompf.com/blog/2012/02/lose-the-wait-http-compression
   101  		// https://web.archive.org/web/20120321182910/http://www.vervestudios.co/projects/compression-tests/results
   102  		//
   103  		// That's why we prefer gzip over deflate. It's just more reliable
   104  		// and not significantly slower than gzip.
   105  		return encodingDeflate
   106  
   107  		// NOTE: Not implemented, intentionally:
   108  		// case "compress": // LZW. Deprecated.
   109  		// case "bzip2":    // Too slow on-the-fly.
   110  		// case "zopfli":   // Too slow on-the-fly.
   111  		// case "xz":       // Too slow on-the-fly.
   112  	}
   113  
   114  	return encodingNone
   115  }
   116  
   117  type maybeCompressResponseWriter struct {
   118  	http.ResponseWriter
   119  	w            io.Writer
   120  	encoding     encoding
   121  	contentTypes map[string]struct{}
   122  	level        int
   123  	wroteHeader  bool
   124  }
   125  
   126  func (w *maybeCompressResponseWriter) WriteHeader(code int) {
   127  	if w.wroteHeader {
   128  		return
   129  	}
   130  	w.wroteHeader = true
   131  	defer w.ResponseWriter.WriteHeader(code)
   132  
   133  	// Already compressed data?
   134  	if w.ResponseWriter.Header().Get("Content-Encoding") != "" {
   135  		return
   136  	}
   137  	// The content-length after compression is unknown
   138  	w.ResponseWriter.Header().Del("Content-Length")
   139  
   140  	// Parse the first part of the Content-Type response header.
   141  	contentType := ""
   142  	parts := strings.Split(w.ResponseWriter.Header().Get("Content-Type"), ";")
   143  	if len(parts) > 0 {
   144  		contentType = parts[0]
   145  	}
   146  
   147  	// Is the content type compressable?
   148  	if _, ok := w.contentTypes[contentType]; !ok {
   149  		return
   150  	}
   151  
   152  	// Select the compress writer.
   153  	switch w.encoding {
   154  	case encodingGzip:
   155  		gw, err := gzip.NewWriterLevel(w.ResponseWriter, w.level)
   156  		if err != nil {
   157  			w.w = w.ResponseWriter
   158  			return
   159  		}
   160  		w.w = gw
   161  		w.ResponseWriter.Header().Set("Content-Encoding", "gzip")
   162  
   163  	case encodingDeflate:
   164  		dw, err := flate.NewWriter(w.ResponseWriter, w.level)
   165  		if err != nil {
   166  			w.w = w.ResponseWriter
   167  			return
   168  		}
   169  		w.w = dw
   170  		w.ResponseWriter.Header().Set("Content-Encoding", "deflate")
   171  	}
   172  }
   173  
   174  func (w *maybeCompressResponseWriter) Write(p []byte) (int, error) {
   175  	if !w.wroteHeader {
   176  		w.WriteHeader(http.StatusOK)
   177  	}
   178  
   179  	return w.w.Write(p)
   180  }
   181  
   182  func (w *maybeCompressResponseWriter) Flush() {
   183  	if f, ok := w.w.(http.Flusher); ok {
   184  		f.Flush()
   185  	}
   186  }
   187  
   188  func (w *maybeCompressResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
   189  	if hj, ok := w.w.(http.Hijacker); ok {
   190  		return hj.Hijack()
   191  	}
   192  	return nil, nil, errors.New("chi/middleware: http.Hijacker is unavailable on the writer")
   193  }
   194  
   195  func (w *maybeCompressResponseWriter) CloseNotify() <-chan bool {
   196  	if cn, ok := w.w.(http.CloseNotifier); ok {
   197  		return cn.CloseNotify()
   198  	}
   199  
   200  	// If the underlying writer does not implement http.CloseNotifier, return
   201  	// a channel that never receives a value. The semantics here is that the
   202  	// client never disconnnects before the request is processed by the
   203  	// http.Handler, which is close enough to the default behavior (when
   204  	// CloseNotify() is not even called).
   205  	return make(chan bool, 1)
   206  }
   207  
   208  func (w *maybeCompressResponseWriter) Close() error {
   209  	if c, ok := w.w.(io.WriteCloser); ok {
   210  		return c.Close()
   211  	}
   212  	return errors.New("chi/middleware: io.WriteCloser is unavailable on the writer")
   213  }