forked from ClusterLabs/hawk-apiserver
-
Notifications
You must be signed in to change notification settings - Fork 0
/
gziphandler.go
223 lines (189 loc) · 5.74 KB
/
gziphandler.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
package main
// Original code under Apache 2.0 license.
import (
"bufio"
"compress/gzip"
"fmt"
"io"
"net"
"net/http"
"strconv"
"strings"
)
// GzipHandler
//
// A http.Handler function which provides gzip compression to request
// responses. Simply create the handler using NewGzipHandler and
// chain it like any other http.Handler function.
//
// This is a stripped down version of github.com/NYTimes/gziphandler.
const (
// Only enable gzip compression if we have at least
// minSize bytes of data to compress
minSize = 512
)
type GzipResponseWriter struct {
http.ResponseWriter
writer *gzip.Writer
code int
buf []byte
}
type codings map[string]float64
func (w *GzipResponseWriter) Write(b []byte) (int, error) {
// set content type
if _, ok := w.Header()["Content-Type"]; !ok {
w.Header().Set("Content-Type", http.DetectContentType(b))
}
if w.writer != nil {
n, err := w.writer.Write(b)
return n, err
}
// save the data to be written later
w.buf = append(w.buf, b...)
// only enable compression if write is >= minSize
// and compression isn't already enabled
if w.Header().Get("Content-Encoding") == "" && len(w.buf) >= minSize {
err := w.startGzip()
if err != nil {
return 0, err
}
}
return len(b), nil
}
func (w *GzipResponseWriter) startGzip() error {
w.Header().Set("Content-Encoding", "gzip")
// if the Content-Length is already set, then calls to Write on gzip
// will fail to set the Content-Length header since its already set
// See: https://github.com/golang/go/issues/14975.
w.Header().Del("Content-Length")
if w.code != 0 {
w.ResponseWriter.WriteHeader(w.code)
}
// Bytes written during ServeHTTP are redirected to this gzip writer
// before being written to the underlying response.
if w.writer == nil {
w.writer = gzip.NewWriter(nil)
}
w.writer.Reset(w.ResponseWriter)
// Flush the buffer into the gzip response.
n, err := w.writer.Write(w.buf)
// This should never happen (per io.Writer docs), but if the write didn't
// accept the entire buffer but returned no specific error, we have no clue
// what's going on, so abort just to be safe.
if err == nil && n < len(w.buf) {
return io.ErrShortWrite
}
w.buf = nil
return err
}
func (w *GzipResponseWriter) WriteHeader(code int) {
// Just save the response code until close / actual write.
w.code = code
}
// Close the writer but keep it around for reuse.
func (w *GzipResponseWriter) Close() error {
if w.writer == nil {
// Gzip not trigged yet, write out regular response.
if w.code != 0 {
w.ResponseWriter.WriteHeader(w.code)
}
if w.buf != nil {
_, writeErr := w.ResponseWriter.Write(w.buf)
// Returns the error if any at write.
if writeErr != nil {
return fmt.Errorf("gziphandler: write to regular responseWriter at close gets error: %q", writeErr.Error())
}
}
return nil
}
return w.writer.Close()
}
// Flush flushes the underlying *gzip.Writer and then the underlying
// http.ResponseWriter if it is an http.Flusher. This makes GzipResponseWriter
// an http.Flusher.
func (w *GzipResponseWriter) Flush() {
if w.writer != nil {
w.writer.Flush()
}
if fw, ok := w.ResponseWriter.(http.Flusher); ok {
fw.Flush()
}
}
// Hijack implements http.Hijacker. If the underlying ResponseWriter is a
// Hijacker, its Hijack method is returned. Otherwise an error is returned.
func (w *GzipResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
if hj, ok := w.ResponseWriter.(http.Hijacker); ok {
return hj.Hijack()
}
return nil, nil, fmt.Errorf("http.Hijacker interface is not supported")
}
// verify Hijacker interface implementation
var _ http.Hijacker = &GzipResponseWriter{}
func NewGzipHandler(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Vary", "Accept-Encoding")
if acceptsGzip(r) {
gw := &GzipResponseWriter{
ResponseWriter: w,
}
defer gw.Close()
h.ServeHTTP(gw, r)
} else {
h.ServeHTTP(w, r)
}
})
}
// acceptsGzip returns true if the given HTTP request indicates that it will
// accept a gzipped response.
func acceptsGzip(r *http.Request) bool {
acceptedEncodings, _ := parseEncodings(r.Header.Get("Accept-Encoding"))
return acceptedEncodings["gzip"] > 0.0
}
// parseEncodings attempts to parse a list of codings, per RFC 2616, as might
// appear in an Accept-Encoding header. It returns a map of content-codings to
// quality values, and an error containing the errors encountered. It's probably
// safe to ignore those, because silently ignoring errors is how the internet
// works.
//
// See: http://tools.ietf.org/html/rfc2616#section-14.3.
func parseEncodings(s string) (codings, error) {
c := make(codings)
var e []string
for _, ss := range strings.Split(s, ",") {
coding, qvalue, err := parseCoding(ss)
if err != nil {
e = append(e, err.Error())
} else {
c[coding] = qvalue
}
}
// TODO (adammck): Use a proper multi-error struct, so the individual errors
// can be extracted if anyone cares.
if len(e) > 0 {
return c, fmt.Errorf("errors while parsing encodings: %s", strings.Join(e, ", "))
}
return c, nil
}
// parseCoding parses a single conding (content-coding with an optional qvalue),
// as might appear in an Accept-Encoding header. It attempts to forgive minor
// formatting errors.
func parseCoding(s string) (coding string, qvalue float64, err error) {
for n, part := range strings.Split(s, ";") {
part = strings.TrimSpace(part)
qvalue = 1.0
if n == 0 {
coding = strings.ToLower(part)
} else if strings.HasPrefix(part, "q=") {
qvalue, err = strconv.ParseFloat(strings.TrimPrefix(part, "q="), 64)
if qvalue < 0.0 {
qvalue = 0.0
} else if qvalue > 1.0 {
qvalue = 1.0
}
}
}
if coding == "" {
err = fmt.Errorf("empty content-coding")
}
return
}