blob: 625cd5c8d3afd237513c91679b4c74da5cc1fbf2 [file] [log] [blame]
Matthias Andreas Benkard832a54e2019-01-29 09:27:38 +01001/*
2Copyright 2017 The Kubernetes Authors.
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8 http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16
17package filters
18
19import (
20 "compress/gzip"
21 "compress/zlib"
22 "errors"
23 "fmt"
24 "io"
25 "net/http"
26 "strings"
27
28 "github.com/emicklei/go-restful"
29
30 "k8s.io/apimachinery/pkg/util/runtime"
31 "k8s.io/apiserver/pkg/endpoints/request"
32)
33
34// Compressor is an interface to compression writers
35type Compressor interface {
36 io.WriteCloser
37 Flush() error
38}
39
40const (
41 headerAcceptEncoding = "Accept-Encoding"
42 headerContentEncoding = "Content-Encoding"
43
44 encodingGzip = "gzip"
45 encodingDeflate = "deflate"
46)
47
48// WithCompression wraps an http.Handler with the Compression Handler
49func WithCompression(handler http.Handler) http.Handler {
50 return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
51 wantsCompression, encoding := wantsCompressedResponse(req)
52 w.Header().Set("Vary", "Accept-Encoding")
53 if wantsCompression {
54 compressionWriter, err := NewCompressionResponseWriter(w, encoding)
55 if err != nil {
56 handleError(w, req, err)
57 runtime.HandleError(fmt.Errorf("failed to compress HTTP response: %v", err))
58 return
59 }
60 compressionWriter.Header().Set("Content-Encoding", encoding)
61 handler.ServeHTTP(compressionWriter, req)
62 compressionWriter.(*compressionResponseWriter).Close()
63 } else {
64 handler.ServeHTTP(w, req)
65 }
66 })
67}
68
69// wantsCompressedResponse reads the Accept-Encoding header to see if and which encoding is requested.
70func wantsCompressedResponse(req *http.Request) (bool, string) {
71 // don't compress watches
72 ctx := req.Context()
73 info, ok := request.RequestInfoFrom(ctx)
74 if !ok {
75 return false, ""
76 }
77 if !info.IsResourceRequest {
78 return false, ""
79 }
80 if info.Verb == "watch" {
81 return false, ""
82 }
83 header := req.Header.Get(headerAcceptEncoding)
84 gi := strings.Index(header, encodingGzip)
85 zi := strings.Index(header, encodingDeflate)
86 // use in order of appearance
87 switch {
88 case gi == -1:
89 return zi != -1, encodingDeflate
90 case zi == -1:
91 return gi != -1, encodingGzip
92 case gi < zi:
93 return true, encodingGzip
94 default:
95 return true, encodingDeflate
96 }
97}
98
99type compressionResponseWriter struct {
100 writer http.ResponseWriter
101 compressor Compressor
102 encoding string
103}
104
105// NewCompressionResponseWriter returns wraps w with a compression ResponseWriter, using the given encoding
106func NewCompressionResponseWriter(w http.ResponseWriter, encoding string) (http.ResponseWriter, error) {
107 var compressor Compressor
108 switch encoding {
109 case encodingGzip:
110 compressor = gzip.NewWriter(w)
111 case encodingDeflate:
112 compressor = zlib.NewWriter(w)
113 default:
114 return nil, fmt.Errorf("%s is not a supported encoding type", encoding)
115 }
116 return &compressionResponseWriter{
117 writer: w,
118 compressor: compressor,
119 encoding: encoding,
120 }, nil
121}
122
123// compressionResponseWriter implements http.Responsewriter Interface
124var _ http.ResponseWriter = &compressionResponseWriter{}
125
126func (c *compressionResponseWriter) Header() http.Header {
127 return c.writer.Header()
128}
129
130// compress data according to compression method
131func (c *compressionResponseWriter) Write(p []byte) (int, error) {
132 if c.compressorClosed() {
133 return -1, errors.New("compressing error: tried to write data using closed compressor")
134 }
135 c.Header().Set(headerContentEncoding, c.encoding)
136 defer c.compressor.Flush()
137 return c.compressor.Write(p)
138}
139
140func (c *compressionResponseWriter) WriteHeader(status int) {
141 c.writer.WriteHeader(status)
142}
143
144// CloseNotify is part of http.CloseNotifier interface
145func (c *compressionResponseWriter) CloseNotify() <-chan bool {
146 return c.writer.(http.CloseNotifier).CloseNotify()
147}
148
149// Close the underlying compressor
150func (c *compressionResponseWriter) Close() error {
151 if c.compressorClosed() {
152 return errors.New("Compressing error: tried to close already closed compressor")
153 }
154
155 c.compressor.Close()
156 c.compressor = nil
157 return nil
158}
159
160func (c *compressionResponseWriter) Flush() {
161 if c.compressorClosed() {
162 return
163 }
164 c.compressor.Flush()
165}
166
167func (c *compressionResponseWriter) compressorClosed() bool {
168 return nil == c.compressor
169}
170
171// RestfulWithCompression wraps WithCompression to be compatible with go-restful
172func RestfulWithCompression(function restful.RouteFunction) restful.RouteFunction {
173 return restful.RouteFunction(func(request *restful.Request, response *restful.Response) {
174 handler := WithCompression(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
175 response.ResponseWriter = w
176 request.Request = req
177 function(request, response)
178 }))
179 handler.ServeHTTP(response.ResponseWriter, request.Request)
180 })
181}