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 }