This repository has been archived by the owner on Mar 29, 2021. It is now read-only.
forked from jmhodges/howsmyssl
-
Notifications
You must be signed in to change notification settings - Fork 0
/
gzip.go
168 lines (151 loc) · 4.95 KB
/
gzip.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
package gzip
import (
"compress/gzip"
"io"
"net/http"
)
// Thanks to Andrew Gerrand for inspiration:
// https://groups.google.com/d/msg/golang-nuts/eVnTcMwNVjM/4vYU8id9Q2UJ
//
// Also, node's Connect library implementation of the compress middleware:
// https://github.com/senchalabs/connect/blob/master/lib/middleware/compress.js
//
// And StackOverflow's explanation of Vary: Accept-Encoding header:
// http://stackoverflow.com/questions/7848796/what-does-varyaccept-encoding-mean
// Internal gzipped writer that satisfies both the (body) writer in gzipped format,
// and maintains the rest of the ResponseWriter interface for header manipulation.
type gzipResponseWriter struct {
io.Writer
http.ResponseWriter
r *http.Request // Keep a hold of the Request, for the filter function
filtered bool // Has the request been run through the filter function?
dogzip bool // Should we do GZIP compression for this request?
filterFn func(http.ResponseWriter, *http.Request) bool
}
// Make sure the filter function is applied.
func (w *gzipResponseWriter) applyFilter() {
if !w.filtered {
if w.dogzip = w.filterFn(w, w.r); w.dogzip {
setGzipHeaders(w.Header())
}
w.filtered = true
}
}
// Unambiguous Write() implementation (otherwise both ResponseWriter and Writer
// want to claim this method).
func (w *gzipResponseWriter) Write(b []byte) (int, error) {
w.applyFilter()
if w.dogzip {
// Write compressed
return w.Writer.Write(b)
}
// Write uncompressed
return w.ResponseWriter.Write(b)
}
// Intercept the WriteHeader call to correctly set the GZIP headers.
func (w *gzipResponseWriter) WriteHeader(code int) {
w.applyFilter()
w.ResponseWriter.WriteHeader(code)
}
// Implement WrapWriter interface
func (w *gzipResponseWriter) WrappedWriter() http.ResponseWriter {
return w.ResponseWriter
}
var (
defaultFilterTypes = [...]string{
"text",
"javascript",
"json",
}
)
// Default filter to check if the response should be GZIPped.
// By default, all text (html, css, xml, ...), javascript and json
// content types are candidates for GZIP.
func defaultFilter(w http.ResponseWriter, r *http.Request) bool {
hdr := w.Header()
for _, tp := range defaultFilterTypes {
ok := HeaderMatch(hdr, "Content-Type", HmContains, tp)
if ok {
return true
}
}
return false
}
// GZIPHandlerFunc is the same as GZIPHandler, it is just a convenience
// signature that accepts a func(http.ResponseWriter, *http.Request) instead of
// a http.Handler interface. It saves the boilerplate http.HandlerFunc() cast.
func GZIPHandlerFunc(h http.HandlerFunc, filterFn func(http.ResponseWriter, *http.Request) bool) http.HandlerFunc {
return GZIPHandler(h, filterFn)
}
// Gzip compression HTTP handler. If the client supports it, it compresses the response
// written by the wrapped handler. The filter function is called when the response is about
// to be written to determine if compression should be applied. If this argument is nil,
// the default filter will GZIP only content types containing /json|text|javascript/.
func GZIPHandler(h http.Handler, filterFn func(http.ResponseWriter, *http.Request) bool) http.HandlerFunc {
if filterFn == nil {
filterFn = defaultFilter
}
return func(w http.ResponseWriter, r *http.Request) {
if _, ok := getGzipWriter(w); ok {
// Self-awareness, gzip handler is already set up
h.ServeHTTP(w, r)
return
}
hdr := w.Header()
setVaryHeader(hdr)
// Do nothing on a HEAD request
if r.Method == "HEAD" {
h.ServeHTTP(w, r)
return
}
if !acceptsGzip(r.Header) {
// No gzip support from the client, return uncompressed
h.ServeHTTP(w, r)
return
}
// Prepare a gzip response container
gz := gzip.NewWriter(w)
gzw := &gzipResponseWriter{
Writer: gz,
ResponseWriter: w,
r: r,
filterFn: filterFn,
}
h.ServeHTTP(gzw, r)
// Iff the handler completed successfully (no panic) and GZIP was indeed used, close the gzip writer,
// which seems to generate a Write to the underlying writer.
if gzw.dogzip {
gz.Close()
}
}
}
// Add the vary by "accept-encoding" header if it is not already set.
func setVaryHeader(hdr http.Header) {
if !HeaderMatch(hdr, "Vary", HmContains, "accept-encoding") {
hdr.Add("Vary", "Accept-Encoding")
}
}
// Checks if the client accepts GZIP-encoded responses.
func acceptsGzip(hdr http.Header) bool {
ok := HeaderMatch(hdr, "Accept-Encoding", HmContains, "gzip")
if !ok {
ok = HeaderMatch(hdr, "Accept-Encoding", HmEquals, "*")
}
return ok
}
func setGzipHeaders(hdr http.Header) {
// The content-type will be explicitly set somewhere down the path of handlers
hdr.Set("Content-Encoding", "gzip")
hdr.Del("Content-Length")
}
// Helper function to retrieve the gzip writer.
func getGzipWriter(w http.ResponseWriter) (*gzipResponseWriter, bool) {
gz, ok := GetResponseWriter(w, func(tst http.ResponseWriter) bool {
_, ok := tst.(*gzipResponseWriter)
return ok
})
if ok {
return gz.(*gzipResponseWriter), true
}
return nil, false
}