github.com/avenga/couper@v1.12.2/server/writer/gzip.go (about)

     1  package writer
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"compress/gzip"
     7  	"fmt"
     8  	"net"
     9  	"net/http"
    10  	"regexp"
    11  )
    12  
    13  const (
    14  	AcceptEncodingHeader  = "Accept-Encoding"
    15  	ContentEncodingHeader = "Content-Encoding"
    16  	ContentLengthHeader   = "Content-Length"
    17  	GzipName              = "gzip"
    18  	VaryHeader            = "Vary"
    19  
    20  	minCompressBodyLength = 60
    21  )
    22  
    23  var (
    24  	clientSupportsGZ = regexp.MustCompile(`(?i)\b` + GzipName + `\b`)
    25  
    26  	_ writer = &Gzip{}
    27  )
    28  
    29  type Gzip struct {
    30  	buffer     *bytes.Buffer
    31  	enabled    bool
    32  	hijacked   bool
    33  	headerSent bool
    34  	statusCode int
    35  	writeErr   error
    36  	rw         http.ResponseWriter
    37  	w          *gzip.Writer
    38  }
    39  
    40  func NewGzipWriter(rw http.ResponseWriter, header http.Header) *Gzip {
    41  	return &Gzip{
    42  		buffer:  bytes.NewBuffer(nil),
    43  		enabled: clientSupportsGZ.MatchString(header.Get(AcceptEncodingHeader)),
    44  		rw:      rw,
    45  		w:       gzip.NewWriter(rw),
    46  	}
    47  }
    48  
    49  // Write fills a small buffer first to determine if a compression is required or not.
    50  func (g *Gzip) Write(p []byte) (n int, err error) {
    51  	b := p[:]
    52  	bytesLen := len(p)
    53  	bufLen := g.buffer.Len()
    54  
    55  	if bufLen < minCompressBodyLength {
    56  		limit := minCompressBodyLength - bufLen
    57  
    58  		if bytesLen < limit {
    59  			return g.buffer.Write(b)
    60  		}
    61  
    62  		// Fill the buffer at least to minCompressBodyLength size.
    63  		if _, err = g.buffer.Write(b); err != nil {
    64  			return 0, err
    65  		}
    66  
    67  		b = g.buffer.Bytes()
    68  	}
    69  
    70  	g.writeHeader()
    71  
    72  	n, err = g.write(b)
    73  	if err != nil {
    74  		return n, err
    75  	} else if bufLen < minCompressBodyLength && bytesLen != (n-bufLen) {
    76  		return 0, fmt.Errorf("invalid write result")
    77  	}
    78  
    79  	return bytesLen, err
    80  }
    81  
    82  func (g *Gzip) write(p []byte) (n int, err error) {
    83  	if g.enabled {
    84  		return g.w.Write(p)
    85  	}
    86  	return g.rw.Write(p)
    87  }
    88  
    89  func (g *Gzip) Close() (err error) {
    90  	if g.writeErr != nil {
    91  		return g.writeErr
    92  	}
    93  
    94  	if g.buffer.Len() < minCompressBodyLength {
    95  		g.enabled = false
    96  		g.writeHeader()
    97  
    98  		_, err = g.write(g.buffer.Bytes())
    99  		if err != nil {
   100  			return err
   101  		}
   102  	}
   103  
   104  	g.writeHeader()
   105  
   106  	if g.enabled && g.w != nil {
   107  		err = g.w.Close()
   108  	}
   109  
   110  	return err
   111  }
   112  
   113  func (g *Gzip) Header() http.Header {
   114  	return g.rw.Header()
   115  }
   116  
   117  func (g *Gzip) WriteHeader(statusCode int) {
   118  	g.statusCode = statusCode
   119  }
   120  
   121  func (g *Gzip) writeHeader() {
   122  	if g.headerSent {
   123  		return
   124  	}
   125  
   126  	g.headerSent = true
   127  
   128  	if g.buffer.Len() >= minCompressBodyLength {
   129  		g.rw.Header().Add(VaryHeader, AcceptEncodingHeader)
   130  	}
   131  
   132  	// With piped upstream bodies there is no gzip reader.
   133  	// Skip client compression if the response is already encoded.
   134  	if g.rw.Header().Get(ContentEncodingHeader) == GzipName {
   135  		g.enabled = false
   136  	}
   137  
   138  	if g.enabled {
   139  		g.rw.Header().Del(ContentLengthHeader)
   140  		g.rw.Header().Set(ContentEncodingHeader, GzipName)
   141  	}
   142  
   143  	if !g.hijacked && g.statusCode > 0 {
   144  		g.rw.WriteHeader(g.statusCode)
   145  	}
   146  }
   147  
   148  func (g *Gzip) Flush() {
   149  	if l := g.buffer.Len(); l < minCompressBodyLength {
   150  		// We have to wait for minCompressBodyLength bytes to be
   151  		// able to determine, if we enable GZIP compression or not.
   152  		return
   153  	}
   154  
   155  	g.writeHeader()
   156  
   157  	if g.enabled && g.w != nil {
   158  		_ = g.w.Flush()
   159  	}
   160  
   161  	if rw, ok := g.rw.(http.Flusher); ok {
   162  		rw.Flush()
   163  	}
   164  }
   165  
   166  func (g *Gzip) Hijack() (net.Conn, *bufio.ReadWriter, error) {
   167  	hijack, ok := g.rw.(http.Hijacker)
   168  	if !ok {
   169  		return nil, nil, fmt.Errorf("can't switch protocols using non-Hijacker gzip writer type %T", g.rw)
   170  	}
   171  
   172  	g.enabled = false
   173  	g.hijacked = true
   174  
   175  	return hijack.Hijack()
   176  }
   177  
   178  func ModifyAcceptEncoding(header http.Header) {
   179  	if clientSupportsGZ.MatchString(header.Get(AcceptEncodingHeader)) {
   180  		header.Set(AcceptEncodingHeader, GzipName)
   181  	} else {
   182  		header.Del(AcceptEncodingHeader)
   183  	}
   184  }