1 // Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
2 // Revel Framework source code and usage is governed by a MIT style
3 // license that can be found in the LICENSE file.
16 var compressionTypes = [...]string{
21 var compressableMimes = [...]string{
28 "application/xhtml+xml",
29 "application/rss+xml",
30 "application/javascript",
31 "application/x-javascript",
34 // Local log instance for this class
35 var compressLog = RevelLog.New("section", "compress")
37 // WriteFlusher interface for compress writer
38 type WriteFlusher interface {
39 io.Writer // An IO Writer
40 io.Closer // A closure
41 Flush() error /// A flush function
44 // The compressed writer
45 type CompressResponseWriter struct {
46 Header *BufferedServerHeader // The header
47 ControllerResponse *Response // The response
48 OriginalWriter io.Writer // The writer
49 compressWriter WriteFlusher // The flushed writer
50 compressionType string // The compression type
51 headersWritten bool // True if written
52 closeNotify chan bool // The notify channel to close
53 parentNotify <-chan bool // The parent chanel to receive the closed event
54 closed bool // True if closed
57 // CompressFilter does compression of response body in gzip/deflate if
58 // `results.compressed=true` in the app.conf
59 func CompressFilter(c *Controller, fc []Filter) {
60 if c.Response.Out.internalHeader.Server != nil && Config.BoolDefault("results.compressed", false) {
61 if c.Response.Status != http.StatusNoContent && c.Response.Status != http.StatusNotModified {
62 if found, compressType, compressWriter := detectCompressionType(c.Request, c.Response); found {
63 writer := CompressResponseWriter{
64 ControllerResponse: c.Response,
65 OriginalWriter: c.Response.GetWriter(),
66 compressWriter: compressWriter,
67 compressionType: compressType,
68 headersWritten: false,
69 closeNotify: make(chan bool, 1),
72 // Swap out the header with our own
73 writer.Header = NewBufferedServerHeader(c.Response.Out.internalHeader.Server)
74 c.Response.Out.internalHeader.Server = writer.Header
75 if w, ok := c.Response.GetWriter().(http.CloseNotifier); ok {
76 writer.parentNotify = w.CloseNotify()
78 c.Response.SetWriter(&writer)
81 compressLog.Debug("CompressFilter: Compression disabled for response ", "status", c.Response.Status)
87 // Called to notify the writer is closing
88 func (c CompressResponseWriter) CloseNotify() <-chan bool {
89 if c.parentNotify != nil {
96 func (c *CompressResponseWriter) cancel() {
100 // Prepare the headers
101 func (c *CompressResponseWriter) prepareHeaders() {
102 if c.compressionType != "" {
104 if t := c.Header.Get("Content-Type"); len(t) > 0 {
107 responseMime = strings.TrimSpace(strings.SplitN(responseMime, ";", 2)[0])
108 shouldEncode := false
110 if len(c.Header.Get("Content-Encoding")) == 0 {
111 for _, compressableMime := range compressableMimes {
112 if responseMime == compressableMime {
114 c.Header.Set("Content-Encoding", c.compressionType)
115 c.Header.Del("Content-Length")
122 c.compressWriter = nil
123 c.compressionType = ""
130 func (c *CompressResponseWriter) WriteHeader(status int) {
134 c.headersWritten = true
136 c.Header.SetStatus(status)
140 func (c *CompressResponseWriter) Close() error {
144 if !c.headersWritten {
147 if c.compressionType != "" {
148 c.Header.Del("Content-Length")
149 if err := c.compressWriter.Close(); err != nil {
150 // TODO When writing directly to stream, an error will be generated
151 compressLog.Error("Close: Error closing compress writer", "type", c.compressionType, "error", err)
155 // Non-blocking write to the closenotifier, if we for some reason should
156 // get called multiple times
158 case c.closeNotify <- true:
165 // Write to the underling buffer
166 func (c *CompressResponseWriter) Write(b []byte) (int, error) {
168 return 0, io.ErrClosedPipe
170 // Abort if parent has been closed
171 if c.parentNotify != nil {
173 case <-c.parentNotify:
174 return 0, io.ErrClosedPipe
178 // Abort if we ourselves have been closed
180 return 0, io.ErrClosedPipe
183 if !c.headersWritten {
185 c.headersWritten = true
187 if c.compressionType != "" {
188 return c.compressWriter.Write(b)
190 return c.OriginalWriter.Write(b)
193 // DetectCompressionType method detects the compression type
194 // from header "Accept-Encoding"
195 func detectCompressionType(req *Request, resp *Response) (found bool, compressionType string, compressionKind WriteFlusher) {
196 if Config.BoolDefault("results.compressed", false) {
197 acceptedEncodings := strings.Split(req.GetHttpHeader("Accept-Encoding"), ",")
200 chosenEncoding := len(compressionTypes)
202 // I have fixed one edge case for issue #914
203 // But it's better to cover all possible edge cases or
204 // Adapt to https://github.com/golang/gddo/blob/master/httputil/header/header.go#L172
205 for _, encoding := range acceptedEncodings {
206 encoding = strings.TrimSpace(encoding)
207 encodingParts := strings.SplitN(encoding, ";", 2)
209 // If we are the format "gzip;q=0.8"
210 if len(encodingParts) > 1 {
211 q := strings.TrimSpace(encodingParts[1])
212 if len(q) == 0 || !strings.HasPrefix(q, "q=") {
217 num, err := strconv.ParseFloat(q[2:], 32)
222 if num >= largestQ && num > 0 {
223 if encodingParts[0] == "*" {
228 for i, encoding := range compressionTypes {
229 if encoding == encodingParts[0] {
230 if i < chosenEncoding {
239 // If we can accept anything, chose our preferred method.
240 if encodingParts[0] == "*" {
245 // This is for just plain "gzip"
246 for i, encoding := range compressionTypes {
247 if encoding == encodingParts[0] {
248 if i < chosenEncoding {
262 compressionType = compressionTypes[chosenEncoding]
264 switch compressionType {
266 compressionKind = gzip.NewWriter(resp.GetWriter())
269 compressionKind = zlib.NewWriter(resp.GetWriter())
276 // BufferedServerHeader will not send content out until the Released is called, from that point on it will act normally
277 // It implements all the ServerHeader
278 type BufferedServerHeader struct {
279 cookieList []string // The cookie list
280 headerMap map[string][]string // The header map
281 status int // The status
282 released bool // True if released
283 original ServerHeader // The original header
286 // Creates a new instance based on the ServerHeader
287 func NewBufferedServerHeader(o ServerHeader) *BufferedServerHeader {
288 return &BufferedServerHeader{original: o, headerMap: map[string][]string{}}
292 func (bsh *BufferedServerHeader) SetCookie(cookie string) {
294 bsh.original.SetCookie(cookie)
296 bsh.cookieList = append(bsh.cookieList, cookie)
301 func (bsh *BufferedServerHeader) GetCookie(key string) (ServerCookie, error) {
302 return bsh.original.GetCookie(key)
305 // Sets (replace) the header key
306 func (bsh *BufferedServerHeader) Set(key string, value string) {
308 bsh.original.Set(key, value)
310 bsh.headerMap[key] = []string{value}
314 // Add (append) to a key this value
315 func (bsh *BufferedServerHeader) Add(key string, value string) {
317 bsh.original.Set(key, value)
320 if v, found := bsh.headerMap[key]; found {
323 bsh.headerMap[key] = append(old, value)
328 func (bsh *BufferedServerHeader) Del(key string) {
330 bsh.original.Del(key)
332 delete(bsh.headerMap, key)
337 func (bsh *BufferedServerHeader) Get(key string) (value []string) {
339 value = bsh.original.Get(key)
341 if v, found := bsh.headerMap[key]; found && len(v) > 0 {
344 value = bsh.original.Get(key)
350 // Get all header keys
351 func (bsh *BufferedServerHeader) GetKeys() (value []string) {
353 value = bsh.original.GetKeys()
355 value = bsh.original.GetKeys()
356 for key := range bsh.headerMap {
358 for _,v := range value {
365 value = append(value,key)
373 func (bsh *BufferedServerHeader) SetStatus(statusCode int) {
375 bsh.original.SetStatus(statusCode)
377 bsh.status = statusCode
381 // Release the header and push the results to the original
382 func (bsh *BufferedServerHeader) Release() {
384 for k, v := range bsh.headerMap {
385 for _, r := range v {
386 bsh.original.Set(k, r)
389 for _, c := range bsh.cookieList {
390 bsh.original.SetCookie(c)
393 bsh.original.SetStatus(bsh.status)