Merge "Change seba installation for cord 7.0.0"
[iec.git] / src / foundation / api / revel / compress.go
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.
4
5 package revel
6
7 import (
8         "compress/gzip"
9         "compress/zlib"
10         "io"
11         "net/http"
12         "strconv"
13         "strings"
14 )
15
16 var compressionTypes = [...]string{
17         "gzip",
18         "deflate",
19 }
20
21 var compressableMimes = [...]string{
22         "text/plain",
23         "text/html",
24         "text/xml",
25         "text/css",
26         "application/json",
27         "application/xml",
28         "application/xhtml+xml",
29         "application/rss+xml",
30         "application/javascript",
31         "application/x-javascript",
32 }
33
34 // Local log instance for this class
35 var compressLog = RevelLog.New("section", "compress")
36
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
42 }
43
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
55 }
56
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),
70                                         closed:             false,
71                                 }
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()
77                                 }
78                                 c.Response.SetWriter(&writer)
79                         }
80                 } else {
81                         compressLog.Debug("CompressFilter: Compression disabled for response ", "status", c.Response.Status)
82                 }
83         }
84         fc[0](c, fc[1:])
85 }
86
87 // Called to notify the writer is closing
88 func (c CompressResponseWriter) CloseNotify() <-chan bool {
89         if c.parentNotify != nil {
90                 return c.parentNotify
91         }
92         return c.closeNotify
93 }
94
95 // Cancel the writer
96 func (c *CompressResponseWriter) cancel() {
97         c.closed = true
98 }
99
100 // Prepare the headers
101 func (c *CompressResponseWriter) prepareHeaders() {
102         if c.compressionType != "" {
103                 responseMime := ""
104                 if t := c.Header.Get("Content-Type"); len(t) > 0 {
105                         responseMime = t[0]
106                 }
107                 responseMime = strings.TrimSpace(strings.SplitN(responseMime, ";", 2)[0])
108                 shouldEncode := false
109
110                 if len(c.Header.Get("Content-Encoding")) == 0 {
111                         for _, compressableMime := range compressableMimes {
112                                 if responseMime == compressableMime {
113                                         shouldEncode = true
114                                         c.Header.Set("Content-Encoding", c.compressionType)
115                                         c.Header.Del("Content-Length")
116                                         break
117                                 }
118                         }
119                 }
120
121                 if !shouldEncode {
122                         c.compressWriter = nil
123                         c.compressionType = ""
124                 }
125         }
126         c.Header.Release()
127 }
128
129 // Write the headers
130 func (c *CompressResponseWriter) WriteHeader(status int) {
131         if c.closed {
132                 return
133         }
134         c.headersWritten = true
135         c.prepareHeaders()
136         c.Header.SetStatus(status)
137 }
138
139 // Close the writer
140 func (c *CompressResponseWriter) Close() error {
141         if c.closed {
142                 return nil
143         }
144         if !c.headersWritten {
145                 c.prepareHeaders()
146         }
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)
152                 }
153
154         }
155         // Non-blocking write to the closenotifier, if we for some reason should
156         // get called multiple times
157         select {
158         case c.closeNotify <- true:
159         default:
160         }
161         c.closed = true
162         return nil
163 }
164
165 // Write to the underling buffer
166 func (c *CompressResponseWriter) Write(b []byte) (int, error) {
167         if c.closed {
168                 return 0, io.ErrClosedPipe
169         }
170         // Abort if parent has been closed
171         if c.parentNotify != nil {
172                 select {
173                 case <-c.parentNotify:
174                         return 0, io.ErrClosedPipe
175                 default:
176                 }
177         }
178         // Abort if we ourselves have been closed
179         if c.closed {
180                 return 0, io.ErrClosedPipe
181         }
182
183         if !c.headersWritten {
184                 c.prepareHeaders()
185                 c.headersWritten = true
186         }
187         if c.compressionType != "" {
188                 return c.compressWriter.Write(b)
189         }
190         return c.OriginalWriter.Write(b)
191 }
192
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"), ",")
198
199                 largestQ := 0.0
200                 chosenEncoding := len(compressionTypes)
201
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)
208
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=") {
213                                         continue
214                                 }
215
216                                 // Strip off the q=
217                                 num, err := strconv.ParseFloat(q[2:], 32)
218                                 if err != nil {
219                                         continue
220                                 }
221
222                                 if num >= largestQ && num > 0 {
223                                         if encodingParts[0] == "*" {
224                                                 chosenEncoding = 0
225                                                 largestQ = num
226                                                 continue
227                                         }
228                                         for i, encoding := range compressionTypes {
229                                                 if encoding == encodingParts[0] {
230                                                         if i < chosenEncoding {
231                                                                 largestQ = num
232                                                                 chosenEncoding = i
233                                                         }
234                                                         break
235                                                 }
236                                         }
237                                 }
238                         } else {
239                                 // If we can accept anything, chose our preferred method.
240                                 if encodingParts[0] == "*" {
241                                         chosenEncoding = 0
242                                         largestQ = 1
243                                         break
244                                 }
245                                 // This is for just plain "gzip"
246                                 for i, encoding := range compressionTypes {
247                                         if encoding == encodingParts[0] {
248                                                 if i < chosenEncoding {
249                                                         largestQ = 1.0
250                                                         chosenEncoding = i
251                                                 }
252                                                 break
253                                         }
254                                 }
255                         }
256                 }
257
258                 if largestQ == 0 {
259                         return
260                 }
261
262                 compressionType = compressionTypes[chosenEncoding]
263
264                 switch compressionType {
265                 case "gzip":
266                         compressionKind = gzip.NewWriter(resp.GetWriter())
267                         found = true
268                 case "deflate":
269                         compressionKind = zlib.NewWriter(resp.GetWriter())
270                         found = true
271                 }
272         }
273         return
274 }
275
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
284 }
285
286 // Creates a new instance based on the ServerHeader
287 func NewBufferedServerHeader(o ServerHeader) *BufferedServerHeader {
288         return &BufferedServerHeader{original: o, headerMap: map[string][]string{}}
289 }
290
291 // Sets the cookie
292 func (bsh *BufferedServerHeader) SetCookie(cookie string) {
293         if bsh.released {
294                 bsh.original.SetCookie(cookie)
295         } else {
296                 bsh.cookieList = append(bsh.cookieList, cookie)
297         }
298 }
299
300 // Returns a cookie
301 func (bsh *BufferedServerHeader) GetCookie(key string) (ServerCookie, error) {
302         return bsh.original.GetCookie(key)
303 }
304
305 // Sets (replace) the header key
306 func (bsh *BufferedServerHeader) Set(key string, value string) {
307         if bsh.released {
308                 bsh.original.Set(key, value)
309         } else {
310                 bsh.headerMap[key] = []string{value}
311         }
312 }
313
314 // Add (append) to a key this value
315 func (bsh *BufferedServerHeader) Add(key string, value string) {
316         if bsh.released {
317                 bsh.original.Set(key, value)
318         } else {
319                 old := []string{}
320                 if v, found := bsh.headerMap[key]; found {
321                         old = v
322                 }
323                 bsh.headerMap[key] = append(old, value)
324         }
325 }
326
327 // Delete this key
328 func (bsh *BufferedServerHeader) Del(key string) {
329         if bsh.released {
330                 bsh.original.Del(key)
331         } else {
332                 delete(bsh.headerMap, key)
333         }
334 }
335
336 // Get this key
337 func (bsh *BufferedServerHeader) Get(key string) (value []string) {
338         if bsh.released {
339                 value = bsh.original.Get(key)
340         } else {
341                 if v, found := bsh.headerMap[key]; found && len(v) > 0 {
342                         value = v
343                 } else {
344                         value = bsh.original.Get(key)
345                 }
346         }
347         return
348 }
349
350 // Get all header keys
351 func (bsh *BufferedServerHeader) GetKeys() (value []string) {
352         if bsh.released {
353                 value = bsh.original.GetKeys()
354         } else {
355                 value = bsh.original.GetKeys()
356                 for key := range bsh.headerMap {
357                         found := false
358                         for _,v := range value {
359                                 if v==key {
360                                         found = true
361                                         break
362                                 }
363                         }
364                         if !found {
365                                 value = append(value,key)
366                         }
367                 }
368         }
369         return
370 }
371
372 // Set the status
373 func (bsh *BufferedServerHeader) SetStatus(statusCode int) {
374         if bsh.released {
375                 bsh.original.SetStatus(statusCode)
376         } else {
377                 bsh.status = statusCode
378         }
379 }
380
381 // Release the header and push the results to the original
382 func (bsh *BufferedServerHeader) Release() {
383         bsh.released = true
384         for k, v := range bsh.headerMap {
385                 for _, r := range v {
386                         bsh.original.Set(k, r)
387                 }
388         }
389         for _, c := range bsh.cookieList {
390                 bsh.original.SetCookie(c)
391         }
392         if bsh.status > 0 {
393                 bsh.original.SetStatus(bsh.status)
394         }
395 }