@marcel91200 / caddy Mirror · read-only
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 11, 01:00 UTC

master
go 707 lines 21.8 KB
Raw
1 // Copyright 2015 Matthew Holt and The Caddy Authors
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14
15 // Package encode implements an encoder middleware for Caddy. The initial
16 // enhancements related to Accept-Encoding, minimum content length, and
17 // buffer/writer pools were adapted from https://github.com/xi2/httpgzip
18 // then modified heavily to accommodate modular encoders and fix bugs.
19 // Code borrowed from that repository is Copyright (c) 2015 The Httpgzip Authors.
20 package encode
21
22 import (
23 "cmp"
24 "fmt"
25 "io"
26 "iter"
27 "math"
28 "net/http"
29 "slices"
30 "strconv"
31 "strings"
32 "sync"
33
34 "golang.org/x/net/http/httpguts"
35
36 "github.com/caddyserver/caddy/v2"
37 "github.com/caddyserver/caddy/v2/modules/caddyhttp"
38 )
39
40 const sseMediaType = "text/event-stream"
41
42 func init() {
43 caddy.RegisterModule(Encode{})
44 }
45
46 // Encode is a middleware which can encode responses.
47 type Encode struct {
48 // Selection of compression algorithms to choose from. The best one
49 // will be chosen based on the client's Accept-Encoding header.
50 EncodingsRaw caddy.ModuleMap `json:"encodings,omitempty" caddy:"namespace=http.encoders"`
51
52 // If the client has no strong preference, choose these encodings in order.
53 Prefer []string `json:"prefer,omitempty"`
54
55 // Only encode responses that are at least this many bytes long.
56 MinLength int `json:"minimum_length,omitempty"`
57
58 // Only encode responses that match against this ResponseMatcher.
59 // The default is a collection of text-based Content-Type headers.
60 Matcher *caddyhttp.ResponseMatcher `json:"match,omitempty"`
61
62 writerPools map[string]*sync.Pool // TODO: these pools do not get reused through config reloads...
63 }
64
65 // CaddyModule returns the Caddy module information.
66 func (Encode) CaddyModule() caddy.ModuleInfo {
67 return caddy.ModuleInfo{
68 ID: "http.handlers.encode",
69 New: func() caddy.Module { return new(Encode) },
70 }
71 }
72
73 // Provision provisions enc.
74 func (enc *Encode) Provision(ctx caddy.Context) error {
75 mods, err := ctx.LoadModule(enc, "EncodingsRaw")
76 if err != nil {
77 return fmt.Errorf("loading encoder modules: %v", err)
78 }
79 for modName, modIface := range mods.(map[string]any) {
80 err = enc.addEncoding(modIface.(Encoding))
81 if err != nil {
82 return fmt.Errorf("adding encoding %s: %v", modName, err)
83 }
84 }
85 if enc.MinLength == 0 {
86 enc.MinLength = defaultMinLength
87 }
88
89 if enc.Matcher == nil {
90 // common text-based content types
91 // list based on https://developers.cloudflare.com/speed/optimization/content/brotli/content-compression/#compression-between-cloudflare-and-website-visitors
92 enc.Matcher = &caddyhttp.ResponseMatcher{
93 Headers: http.Header{
94 "Content-Type": []string{
95 "application/atom+xml*",
96 "application/eot*",
97 "application/font*",
98 "application/geo+json*",
99 "application/graphql+json*",
100 "application/graphql-response+json*",
101 "application/javascript*",
102 "application/json*",
103 "application/ld+json*",
104 "application/manifest+json*",
105 "application/opentype*",
106 "application/otf*",
107 "application/rss+xml*",
108 "application/truetype*",
109 "application/ttf*",
110 "application/vnd.api+json*",
111 "application/vnd.ms-fontobject*",
112 "application/wasm*",
113 "application/x-httpd-cgi*",
114 "application/x-javascript*",
115 "application/x-opentype*",
116 "application/x-otf*",
117 "application/x-perl*",
118 "application/x-protobuf*",
119 "application/x-ttf*",
120 "application/xhtml+xml*",
121 "application/xml*",
122 "font/ttf*",
123 "font/otf*",
124 "image/svg+xml*",
125 "image/vnd.microsoft.icon*",
126 "image/x-icon*",
127 "multipart/bag*",
128 "multipart/mixed*",
129 "text/*",
130 },
131 },
132 }
133 }
134
135 if len(enc.Prefer) == 0 {
136 for _, encName := range []string{"zstd", "br", "gzip"} {
137 if _, ok := enc.writerPools[encName]; ok {
138 enc.Prefer = append(enc.Prefer, encName)
139 }
140 }
141 }
142
143 return nil
144 }
145
146 // Validate ensures that enc's configuration is valid.
147 func (enc *Encode) Validate() error {
148 check := make(map[string]bool)
149 for _, encName := range enc.Prefer {
150 if _, ok := enc.writerPools[encName]; !ok {
151 return fmt.Errorf("encoding %s not enabled", encName)
152 }
153
154 if _, ok := check[encName]; ok {
155 return fmt.Errorf("encoding %s is duplicated in prefer", encName)
156 }
157 check[encName] = true
158 }
159
160 return nil
161 }
162
163 func hasNonEmptyValue(h http.Header, key string) bool {
164 for _, v := range h[key] {
165 if v != "" {
166 return true
167 }
168 }
169 return false
170 }
171
172 func (enc *Encode) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
173 var encodingName string
174 for _, encName := range AcceptedEncodings(r, enc.Prefer) {
175 if _, ok := enc.writerPools[encName]; !ok {
176 continue // encoding not offered
177 }
178 encodingName = encName
179
180 // to comply with RFC 9110 section 8.8.3(.3), we modify the Etag when encoding
181 // by appending a hyphen and the encoder name; the problem is, the client will
182 // send back that Etag in an If-None-Match header, but upstream handlers that set
183 // the Etag in the first place don't know that we appended to their Etag! so here
184 // we have to strip our addition so the upstream handlers can still honor client
185 // caches without knowing about our changes...
186 if etags := r.Header["If-None-Match"]; len(etags) > 0 {
187 etag := strings.Join(etags, ", ")
188 stripped := strings.ReplaceAll(etag, "-"+encName+`"`, `"`)
189 if stripped != etag || len(etags) > 1 {
190 r.Header["If-None-Match"] = []string{stripped}
191 }
192 }
193
194 break
195 }
196
197 // wrap even when no encoding was negotiated so eligible identity responses
198 // still carry Vary: Accept-Encoding for shared caches
199 w = enc.openResponseWriter(encodingName, w, r.Method == http.MethodConnect)
200 defer w.(*responseWriter).Close()
201
202 err := next.ServeHTTP(w, r)
203 // If there was an error, disable encoding completely
204 // This prevents corruption when handle_errors processes the response
205 if err != nil {
206 if ew, ok := w.(*responseWriter); ok {
207 ew.disabled = true
208 }
209 }
210
211 return err
212 }
213
214 func (enc *Encode) addEncoding(e Encoding) error {
215 ae := e.AcceptEncoding()
216 if ae == "" {
217 return fmt.Errorf("encoder does not specify an Accept-Encoding value")
218 }
219 if _, ok := enc.writerPools[ae]; ok {
220 return fmt.Errorf("encoder already added: %s", ae)
221 }
222 if enc.writerPools == nil {
223 enc.writerPools = make(map[string]*sync.Pool)
224 }
225 enc.writerPools[ae] = &sync.Pool{
226 New: func() any {
227 return e.NewEncoder()
228 },
229 }
230 return nil
231 }
232
233 // openResponseWriter creates a new response writer that may (or may not)
234 // encode the response with encodingName. The returned response writer MUST
235 // be closed after the handler completes.
236 func (enc *Encode) openResponseWriter(encodingName string, w http.ResponseWriter, isConnect bool) *responseWriter {
237 var rw responseWriter
238 return enc.initResponseWriter(&rw, encodingName, w, isConnect)
239 }
240
241 // initResponseWriter initializes the responseWriter instance
242 // allocated in openResponseWriter, enabling mid-stack inlining.
243 func (enc *Encode) initResponseWriter(rw *responseWriter, encodingName string, wrappedRW http.ResponseWriter, isConnect bool) *responseWriter {
244 if rww, ok := wrappedRW.(*caddyhttp.ResponseWriterWrapper); ok {
245 rw.ResponseWriter = rww
246 } else {
247 rw.ResponseWriter = &caddyhttp.ResponseWriterWrapper{ResponseWriter: wrappedRW}
248 }
249 rw.encodingName = encodingName
250 rw.config = enc
251 rw.isConnect = isConnect
252
253 return rw
254 }
255
256 // responseWriter writes to an underlying response writer
257 // using the encoding represented by encodingName and
258 // configured by config.
259 type responseWriter struct {
260 http.ResponseWriter
261 encodingName string // empty when the client accepts no offered encoding
262 w Encoder
263 config *Encode
264 statusCode int
265 wroteHeader bool
266 wroteBody bool
267 incremental bool
268 isConnect bool
269 disabled bool // disable encoding for this response
270 }
271
272 // WriteHeader stores the status to write when the time comes
273 // to actually write the header.
274 func (rw *responseWriter) WriteHeader(status int) {
275 rw.statusCode = status
276 if status == http.StatusPartialContent {
277 rw.disabled = true // partial representations must not be dynamically re-encoded
278 }
279
280 h := rw.Header()
281 finalStatus := status < 100 || status > 199
282 if !rw.wroteHeader && finalStatus {
283 rw.incremental = caddyhttp.IsIncremental(h)
284 }
285
286 // See #5849 and RFC 9110 section 15.4.5 (https://www.rfc-editor.org/rfc/rfc9110.html#section-15.4.5) - 304
287 // Not Modified must have certain headers set as if it was a 200 response, and according to the issue
288 // we would miss the Vary header in this case when compression was also enabled; note that we set this
289 // header in the responseWriter.init() method but that is only called if we are writing a response body
290 if status == http.StatusNotModified && !httpguts.HeaderValuesContainsToken(h["Vary"], "Accept-Encoding") {
291 h["Vary"] = append(h["Vary"], "Accept-Encoding")
292 }
293
294 // write status immediately if status is 2xx and the request is CONNECT
295 // since it means the response is successful.
296 // see: https://github.com/caddyserver/caddy/issues/6733#issuecomment-2525058845
297 if rw.isConnect && 200 <= status && status <= 299 {
298 rw.ResponseWriter.WriteHeader(status)
299 rw.wroteHeader = true
300 }
301
302 // write status immediately when status code is informational
303 // see: https://caddy.community/t/disappear-103-early-hints-response-with-encode-enable-caddy-v2-7-6/23081/5
304 if 100 <= status && status <= 199 {
305 rw.ResponseWriter.WriteHeader(status)
306 }
307
308 // write header immediately for server-sent events responses and for
309 // responses that ask for incremental forwarding (RFC 10036), since the
310 // body may not be written for a while and the client needs the headers
311 // to establish the stream; see #6293
312 if !rw.wroteHeader && finalStatus &&
313 (rw.incremental || isSSE(firstValue(h["Content-Type"]))) {
314 rw.init()
315 rw.ResponseWriter.WriteHeader(status)
316 rw.wroteHeader = true
317 }
318 if rw.incremental && rw.wroteHeader {
319 // WriteHeader alone may remain buffered by net/http. Incremental
320 // forwarding requires the header section to be sent downstream now.
321 _ = http.NewResponseController(rw.ResponseWriter).Flush()
322 }
323 }
324
325 func isSSE(contentType string) bool {
326 if len(contentType) < len(sseMediaType) || !strings.EqualFold(contentType[:len(sseMediaType)], sseMediaType) {
327 return false
328 }
329
330 // After the media type, allow only optional whitespace followed by the
331 // end of the value or a parameter separator, so a longer type such as
332 // "text/event-streamfoo" or garbage like "text/event-stream nonsense"
333 // does not match. TrimLeft on the tail does not allocate.
334 rest := strings.TrimLeft(contentType[len(sseMediaType):], " \t")
335
336 return rest == "" || rest[0] == ';'
337 }
338
339 // Match determines, if encoding should be done based on the ResponseMatcher.
340 func (enc *Encode) Match(rw *responseWriter) bool {
341 return enc.Matcher.Match(rw.statusCode, rw.Header())
342 }
343
344 // FlushError is an alternative Flush returning an error. It delays the actual Flush of the underlying
345 // ResponseWriterWrapper until headers were written.
346 func (rw *responseWriter) FlushError() error {
347 if !rw.wroteHeader && rw.statusCode == 0 && caddyhttp.IsIncremental(rw.Header()) {
348 rw.WriteHeader(http.StatusOK)
349 }
350
351 // WriteHeader wasn't called and is a CONNECT request, treat it as a success.
352 // otherwise, wait until header is written.
353 if rw.isConnect && !rw.wroteHeader && rw.statusCode == 0 {
354 rw.WriteHeader(http.StatusOK)
355 }
356
357 if !rw.wroteHeader {
358 // flushing the underlying ResponseWriter will write header and status code,
359 // but we need to delay that until we can determine if we must encode and
360 // therefore add the Content-Encoding header; this happens in the first call
361 // to rw.Write (see bug in #4314)
362 return nil
363 }
364 // also flushes the encoder, if any
365 // see: https://github.com/jjiang-stripe/caddy-slow-gzip
366 if rw.w != nil {
367 err := rw.w.Flush()
368 if err != nil {
369 return err
370 }
371 }
372 //nolint:bodyclose
373 return http.NewResponseController(rw.ResponseWriter).Flush()
374 }
375
376 // Write writes to the response. If the response qualifies,
377 // it is encoded using the encoder, which is initialized
378 // if not done so already.
379 func (rw *responseWriter) Write(p []byte) (int, error) {
380 // ignore zero data writes, probably head request
381 if len(p) == 0 {
382 return 0, nil
383 }
384
385 // WriteHeader wasn't called and is a CONNECT request, treat it as a success.
386 // otherwise, determine if the response should be compressed.
387 if rw.isConnect && !rw.wroteHeader && rw.statusCode == 0 {
388 rw.WriteHeader(http.StatusOK)
389 }
390 if !rw.wroteHeader && rw.statusCode == 0 && caddyhttp.IsIncremental(rw.Header()) {
391 if hdr := rw.Header(); firstValue(hdr["Content-Type"]) == "" {
392 hdr["Content-Type"] = []string{http.DetectContentType(p)}
393 }
394 rw.WriteHeader(http.StatusOK)
395 }
396
397 // sniff content-type and determine content-length
398 if !rw.wroteHeader && rw.config.MinLength > 0 {
399 var gtMinLength bool
400 if len(p) > rw.config.MinLength {
401 gtMinLength = true
402 } else if cl, err := strconv.Atoi(firstValue(rw.Header()["Content-Length"])); err == nil && cl > rw.config.MinLength {
403 gtMinLength = true
404 }
405
406 if gtMinLength {
407 if hdr := rw.Header(); firstValue(hdr["Content-Type"]) == "" {
408 hdr["Content-Type"] = []string{http.DetectContentType(p)}
409 }
410 rw.init()
411 }
412 }
413
414 // before we write to the response, we need to make
415 // sure the header is written exactly once; we do
416 // that by checking if a status code has been set,
417 // and if so, that means we haven't written the
418 // header OR the default status code will be written
419 // by the standard library
420 if !rw.wroteHeader {
421 if rw.statusCode != 0 {
422 rw.ResponseWriter.WriteHeader(rw.statusCode)
423 }
424 rw.wroteHeader = true
425 }
426
427 var n int
428 var err error
429 if rw.w != nil {
430 n, err = rw.w.Write(p)
431 } else {
432 n, err = rw.ResponseWriter.Write(p)
433 }
434 if n > 0 {
435 rw.wroteBody = true
436 }
437 if err != nil || !rw.incremental {
438 return n, err
439 }
440 return n, rw.FlushError()
441 }
442
443 // used to mask ReadFrom method
444 type writerOnly struct {
445 io.Writer
446 }
447
448 // copied from stdlib
449 const sniffLen = 512
450
451 // ReadFrom will try to use sendfile to copy from the reader to the response writer.
452 // It's only used if the response writer implements io.ReaderFrom and the data can't be compressed.
453 // It's based on the standard library HTTP/1.1 response writer implementation.
454 // https://github.com/golang/go/blob/f4e3ec3dbe3b8e04a058d266adf8e048bab563f2/src/net/http/server.go#L586
455 func (rw *responseWriter) ReadFrom(r io.Reader) (int64, error) {
456 if rw.incremental || !rw.wroteHeader && caddyhttp.IsIncremental(rw.Header()) {
457 return io.Copy(writerOnly{rw}, r)
458 }
459
460 rf, ok := rw.ResponseWriter.(io.ReaderFrom)
461 // sendfile can't be used anyway
462 if !ok {
463 // mask ReadFrom to avoid infinite recursion
464 return io.Copy(writerOnly{rw}, r)
465 }
466
467 var ns int64
468 // try to sniff the content type and determine if the response should be compressed
469 if !rw.wroteHeader && rw.config.MinLength > 0 {
470 var (
471 err error
472 buf [sniffLen]byte
473 )
474 // mask ReadFrom to let Write determine if the response should be compressed
475 ns, err = io.CopyBuffer(writerOnly{rw}, io.LimitReader(r, sniffLen), buf[:])
476 if err != nil || ns < sniffLen {
477 return ns, err
478 }
479 }
480
481 // the response will be compressed, no sendfile support
482 if rw.w != nil {
483 nr, err := io.Copy(rw.w, r)
484 if nr > 0 {
485 rw.wroteBody = true
486 }
487 return nr + ns, err
488 }
489 nr, err := rf.ReadFrom(r)
490 if nr > 0 {
491 rw.wroteBody = true
492 }
493 return nr + ns, err
494 }
495
496 // Close writes any remaining buffered response and
497 // deallocates any active resources.
498 func (rw *responseWriter) Close() error {
499 // didn't write, probably head request
500 if !rw.wroteHeader {
501 cl, err := strconv.Atoi(firstValue(rw.Header()["Content-Length"]))
502 if err == nil && cl > rw.config.MinLength {
503 rw.init()
504 }
505
506 // issue #5059, don't write status code if not set explicitly.
507 if rw.statusCode != 0 {
508 rw.ResponseWriter.WriteHeader(rw.statusCode)
509 }
510 rw.wroteHeader = true
511 }
512
513 var err error
514 if rw.w != nil {
515 // don't write an empty encoded stream when there was no body
516 if rw.wroteBody {
517 err = rw.w.Close()
518 }
519 rw.w.Reset(nil)
520 rw.config.writerPools[rw.encodingName].Put(rw.w)
521 rw.w = nil
522 }
523 return err
524 }
525
526 // Unwrap returns the underlying ResponseWriter.
527 func (rw *responseWriter) Unwrap() http.ResponseWriter {
528 return rw.ResponseWriter
529 }
530
531 // init should be called before we write a response, if rw.buf has contents.
532 func (rw *responseWriter) init() {
533 // Don't initialize encoder for responses that must not be encoded.
534 if rw.disabled {
535 return
536 }
537
538 hdr := rw.Header()
539
540 // Only the response no-transform binds us; the request one addresses intermediaries.
541 if !hasNonEmptyValue(hdr, "Content-Encoding") &&
542 !httpguts.HeaderValuesContainsToken(hdr["Cache-Control"], "no-transform") &&
543 rw.config.Match(rw) {
544 if !httpguts.HeaderValuesContainsToken(hdr["Vary"], "Accept-Encoding") {
545 hdr["Vary"] = append(hdr["Vary"], "Accept-Encoding")
546 }
547 if rw.encodingName == "" {
548 return
549 }
550 rw.w = rw.config.writerPools[rw.encodingName].Get().(Encoder)
551 rw.w.Reset(rw.ResponseWriter)
552 delete(hdr, "Content-Length") // https://github.com/golang/go/issues/14975
553 hdr["Content-Encoding"] = []string{rw.encodingName}
554 delete(hdr, "Accept-Ranges") // we don't know ranges for dynamically-encoded content
555
556 // strong ETags need to be distinct depending on the encoding ("selected representation")
557 // see RFC 9110 section 8.8.3.3:
558 // https://www.rfc-editor.org/rfc/rfc9110.html#name-example-entity-tags-varying
559 // I don't know a great way to do this... how about appending? That's a neat trick!
560 // (We have to strip the value we append from If-None-Match headers before
561 // sending subsequent requests back upstream, however, since upstream handlers
562 // don't know about our appending to their Etag since they've already done their work)
563 if etag := firstValue(hdr["Etag"]); etag != "" && !strings.HasPrefix(etag, "W/") {
564 etag = strings.TrimSuffix(etag, `"`) + "-" + rw.encodingName + `"`
565 hdr["Etag"] = []string{etag}
566 }
567 }
568 }
569
570 // firstValue is http.Header.Get without key normalization, for callers that
571 // index the header with a canonical key.
572 func firstValue(values []string) string {
573 if len(values) > 0 {
574 return values[0]
575 }
576 return ""
577 }
578
579 // AcceptedEncodings returns the list of encodings that the
580 // client supports, in descending order of preference.
581 // The client preference via q-factor and the server
582 // preference via Prefer setting are taken into account. If
583 // the Sec-WebSocket-Key header is present then non-identity
584 // encodings are not considered. See
585 // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html.
586 func AcceptedEncodings(r *http.Request, preferredOrder []string) []string {
587 values := r.Header["Accept-Encoding"]
588 if len(values) == 0 {
589 return []string{}
590 }
591 websocketKey := firstValue(r.Header["Sec-Websocket-Key"])
592
593 // sized for typical headers so the slice stays on the stack
594 prefs := make([]encodingPreference, 0, 8)
595
596 for accepted := range listItems(values) {
597 encName, params, found := strings.Cut(accepted, ";")
598 encName = strings.ToLower(strings.TrimSpace(encName))
599 if encName == "" {
600 continue
601 }
602
603 // determine q-factor
604 qFactor := 1.0
605 if found {
606 qFactorStr, _, _ := strings.Cut(params, ";")
607 qFactorStr = strings.TrimSpace(qFactorStr)
608 if len(qFactorStr) >= 2 && strings.EqualFold(qFactorStr[:2], "q=") {
609 if qFactorFloat, err := strconv.ParseFloat(qFactorStr[2:], 32); err == nil {
610 if qFactorFloat >= 0 && qFactorFloat <= 1 {
611 qFactor = qFactorFloat
612 }
613 }
614 }
615 }
616
617 // encodings with q-factor of 0 are not accepted;
618 // use a small threshold to account for float precision
619 if qFactor < 0.00001 {
620 continue
621 }
622
623 // don't encode WebSocket handshakes
624 if websocketKey != "" && encName != "identity" {
625 continue
626 }
627
628 // set server preference
629 prefOrder := slices.Index(preferredOrder, encName)
630 if prefOrder > -1 {
631 prefOrder = len(preferredOrder) - prefOrder
632 }
633
634 prefs = append(prefs, encodingPreference{
635 encoding: encName,
636 q: qFactor,
637 preferOrder: prefOrder,
638 })
639 }
640
641 // sort preferences by descending q-factor first, then by preferOrder
642 slices.SortStableFunc(prefs, func(a, b encodingPreference) int {
643 if math.Abs(a.q-b.q) < 0.00001 {
644 return cmp.Compare(b.preferOrder, a.preferOrder)
645 }
646 return cmp.Compare(b.q, a.q)
647 })
648
649 prefEncNames := make([]string, len(prefs))
650 for i := range prefs {
651 prefEncNames[i] = prefs[i].encoding
652 }
653
654 return prefEncNames
655 }
656
657 // listItems yields the items of a list-based field, which RFC 9110 section 5.3
658 // allows to be split across several field lines.
659 func listItems(lines []string) iter.Seq[string] {
660 return func(yield func(string) bool) {
661 for _, line := range lines {
662 for item := range strings.SplitSeq(line, ",") {
663 if !yield(item) {
664 return
665 }
666 }
667 }
668 }
669 }
670
671 // encodingPreference pairs an encoding with its q-factor.
672 type encodingPreference struct {
673 encoding string
674 q float64
675 preferOrder int
676 }
677
678 // Encoder is a type which can encode a stream of data.
679 type Encoder interface {
680 io.WriteCloser
681 Reset(io.Writer)
682 Flush() error // encoder by default buffers data to maximize compressing rate
683 }
684
685 // Encoding is a type which can create encoders of its kind
686 // and return the name used in the Accept-Encoding header.
687 type Encoding interface {
688 AcceptEncoding() string
689 NewEncoder() Encoder
690 }
691
692 // Precompressed is a type which returns filename suffix of precompressed
693 // file and Accept-Encoding header to use when serving this file.
694 type Precompressed interface {
695 AcceptEncoding() string
696 Suffix() string
697 }
698
699 // defaultMinLength is the minimum length at which to compress content.
700 const defaultMinLength = 512
701
702 // Interface guards
703 var (
704 _ caddy.Provisioner = (*Encode)(nil)
705 _ caddy.Validator = (*Encode)(nil)
706 _ caddyhttp.MiddlewareHandler = (*Encode)(nil)
707 )