You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							349 lines
						
					
					
						
							10 KiB
						
					
					
				
			
		
		
		
			
			
			
				
					
				
				
					
				
			
		
		
	
	
							349 lines
						
					
					
						
							10 KiB
						
					
					
				| // Copyright 2015 Matthew Holt and The Caddy Authors | |
| // | |
| // Licensed under the Apache License, Version 2.0 (the "License"); | |
| // you may not use this file except in compliance with the License. | |
| // You may obtain a copy of the License at | |
| // | |
| //     http://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, software | |
| // distributed under the License is distributed on an "AS IS" BASIS, | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| // See the License for the specific language governing permissions and | |
| // limitations under the License. | |
|  | |
| // Package encode implements an encoder middleware for Caddy. The initial | |
| // enhancements related to Accept-Encoding, minimum content length, and | |
| // buffer/writer pools were adapted from https://github.com/xi2/httpgzip | |
| // then modified heavily to accommodate modular encoders and fix bugs. | |
| // Code borrowed from that repository is Copyright (c) 2015 The Httpgzip Authors. | |
| package encode | |
| 
 | |
| import ( | |
| 	"bytes" | |
| 	"fmt" | |
| 	"io" | |
| 	"net/http" | |
| 	"sort" | |
| 	"strconv" | |
| 	"strings" | |
| 	"sync" | |
| 
 | |
| 	"github.com/caddyserver/caddy/v2" | |
| 	"github.com/caddyserver/caddy/v2/modules/caddyhttp" | |
| ) | |
| 
 | |
| func init() { | |
| 	caddy.RegisterModule(Encode{}) | |
| } | |
| 
 | |
| // Encode is a middleware which can encode responses. | |
| type Encode struct { | |
| 	// Selection of compression algorithms to choose from. The best one | |
| 	// will be chosen based on the client's Accept-Encoding header. | |
| 	EncodingsRaw caddy.ModuleMap `json:"encodings,omitempty" caddy:"namespace=http.encoders"` | |
| 
 | |
| 	// If the client has no strong preference, choose this encoding. TODO: Not yet implemented | |
| 	// Prefer    []string `json:"prefer,omitempty"` | |
|  | |
| 	// Only encode responses that are at least this many bytes long. | |
| 	MinLength int `json:"minimum_length,omitempty"` | |
| 
 | |
| 	writerPools map[string]*sync.Pool // TODO: these pools do not get reused through config reloads... | |
| } | |
| 
 | |
| // CaddyModule returns the Caddy module information. | |
| func (Encode) CaddyModule() caddy.ModuleInfo { | |
| 	return caddy.ModuleInfo{ | |
| 		ID:  "http.handlers.encode", | |
| 		New: func() caddy.Module { return new(Encode) }, | |
| 	} | |
| } | |
| 
 | |
| // Provision provisions enc. | |
| func (enc *Encode) Provision(ctx caddy.Context) error { | |
| 	mods, err := ctx.LoadModule(enc, "EncodingsRaw") | |
| 	if err != nil { | |
| 		return fmt.Errorf("loading encoder modules: %v", err) | |
| 	} | |
| 	for modName, modIface := range mods.(map[string]interface{}) { | |
| 		err = enc.addEncoding(modIface.(Encoding)) | |
| 		if err != nil { | |
| 			return fmt.Errorf("adding encoding %s: %v", modName, err) | |
| 		} | |
| 	} | |
| 	if enc.MinLength == 0 { | |
| 		enc.MinLength = defaultMinLength | |
| 	} | |
| 	return nil | |
| } | |
| 
 | |
| func (enc *Encode) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error { | |
| 	for _, encName := range acceptedEncodings(r) { | |
| 		if _, ok := enc.writerPools[encName]; !ok { | |
| 			continue // encoding not offered | |
| 		} | |
| 		w = enc.openResponseWriter(encName, w) | |
| 		defer w.(*responseWriter).Close() | |
| 		break | |
| 	} | |
| 	return next.ServeHTTP(w, r) | |
| } | |
| 
 | |
| func (enc *Encode) addEncoding(e Encoding) error { | |
| 	ae := e.AcceptEncoding() | |
| 	if ae == "" { | |
| 		return fmt.Errorf("encoder does not specify an Accept-Encoding value") | |
| 	} | |
| 	if _, ok := enc.writerPools[ae]; ok { | |
| 		return fmt.Errorf("encoder already added: %s", ae) | |
| 	} | |
| 	if enc.writerPools == nil { | |
| 		enc.writerPools = make(map[string]*sync.Pool) | |
| 	} | |
| 	enc.writerPools[ae] = &sync.Pool{ | |
| 		New: func() interface{} { | |
| 			return e.NewEncoder() | |
| 		}, | |
| 	} | |
| 	return nil | |
| } | |
| 
 | |
| // openResponseWriter creates a new response writer that may (or may not) | |
| // encode the response with encodingName. The returned response writer MUST | |
| // be closed after the handler completes. | |
| func (enc *Encode) openResponseWriter(encodingName string, w http.ResponseWriter) *responseWriter { | |
| 	var rw responseWriter | |
| 	return enc.initResponseWriter(&rw, encodingName, w) | |
| } | |
| 
 | |
| // initResponseWriter initializes the responseWriter instance | |
| // allocated in openResponseWriter, enabling mid-stack inlining. | |
| func (enc *Encode) initResponseWriter(rw *responseWriter, encodingName string, wrappedRW http.ResponseWriter) *responseWriter { | |
| 	buf := bufPool.Get().(*bytes.Buffer) | |
| 	buf.Reset() | |
| 
 | |
| 	// The allocation of ResponseWriterWrapper might be optimized as well. | |
| 	rw.ResponseWriterWrapper = &caddyhttp.ResponseWriterWrapper{ResponseWriter: wrappedRW} | |
| 	rw.encodingName = encodingName | |
| 	rw.buf = buf | |
| 	rw.config = enc | |
| 
 | |
| 	return rw | |
| } | |
| 
 | |
| // responseWriter writes to an underlying response writer | |
| // using the encoding represented by encodingName and | |
| // configured by config. | |
| type responseWriter struct { | |
| 	*caddyhttp.ResponseWriterWrapper | |
| 	encodingName string | |
| 	w            Encoder | |
| 	buf          *bytes.Buffer | |
| 	config       *Encode | |
| 	statusCode   int | |
| } | |
| 
 | |
| // WriteHeader stores the status to write when the time comes | |
| // to actually write the header. | |
| func (rw *responseWriter) WriteHeader(status int) { | |
| 	rw.statusCode = status | |
| } | |
| 
 | |
| // Write writes to the response. If the response qualifies, | |
| // it is encoded using the encoder, which is initialized | |
| // if not done so already. | |
| func (rw *responseWriter) Write(p []byte) (int, error) { | |
| 	var n, written int | |
| 	var err error | |
| 
 | |
| 	if rw.buf != nil && rw.config.MinLength > 0 { | |
| 		written = rw.buf.Len() | |
| 		_, err := rw.buf.Write(p) | |
| 		if err != nil { | |
| 			return 0, err | |
| 		} | |
| 		rw.init() | |
| 		p = rw.buf.Bytes() | |
| 		defer func() { | |
| 			bufPool.Put(rw.buf) | |
| 			rw.buf = nil | |
| 		}() | |
| 	} | |
| 
 | |
| 	// before we write to the response, we need to make | |
| 	// sure the header is written exactly once; we do | |
| 	// that by checking if a status code has been set, | |
| 	// and if so, that means we haven't written the | |
| 	// header OR the default status code will be written | |
| 	// by the standard library | |
| 	if rw.statusCode > 0 { | |
| 		rw.ResponseWriter.WriteHeader(rw.statusCode) | |
| 		rw.statusCode = 0 | |
| 	} | |
| 
 | |
| 	switch { | |
| 	case rw.w != nil: | |
| 		n, err = rw.w.Write(p) | |
| 	default: | |
| 		n, err = rw.ResponseWriter.Write(p) | |
| 	} | |
| 	n -= written | |
| 	if n < 0 { | |
| 		n = 0 | |
| 	} | |
| 	return n, err | |
| } | |
| 
 | |
| // Close writes any remaining buffered response and | |
| // deallocates any active resources. | |
| func (rw *responseWriter) Close() error { | |
| 	var err error | |
| 	// only attempt to write the remaining buffered response | |
| 	// if there are any bytes left to write; otherwise, if | |
| 	// the handler above us returned an error without writing | |
| 	// anything, we'd write to the response when we instead | |
| 	// should simply let the error propagate back down; this | |
| 	// is why the check for rw.buf.Len() > 0 is crucial | |
| 	if rw.buf != nil && rw.buf.Len() > 0 { | |
| 		rw.init() | |
| 		p := rw.buf.Bytes() | |
| 		defer func() { | |
| 			bufPool.Put(rw.buf) | |
| 			rw.buf = nil | |
| 		}() | |
| 		switch { | |
| 		case rw.w != nil: | |
| 			_, err = rw.w.Write(p) | |
| 		default: | |
| 			_, err = rw.ResponseWriter.Write(p) | |
| 		} | |
| 	} else if rw.statusCode != 0 { | |
| 		// it is possible that a body was not written, and | |
| 		// a header was not even written yet, even though | |
| 		// we are closing; ensure the proper status code is | |
| 		// written exactly once, or we risk breaking requests | |
| 		// that rely on If-None-Match, for example | |
| 		rw.ResponseWriter.WriteHeader(rw.statusCode) | |
| 		rw.statusCode = 0 | |
| 	} | |
| 	if rw.w != nil { | |
| 		err2 := rw.w.Close() | |
| 		if err2 != nil && err == nil { | |
| 			err = err2 | |
| 		} | |
| 		rw.config.writerPools[rw.encodingName].Put(rw.w) | |
| 		rw.w = nil | |
| 	} | |
| 	return err | |
| } | |
| 
 | |
| // init should be called before we write a response, if rw.buf has contents. | |
| func (rw *responseWriter) init() { | |
| 	if rw.Header().Get("Content-Encoding") == "" && rw.buf.Len() >= rw.config.MinLength { | |
| 		rw.w = rw.config.writerPools[rw.encodingName].Get().(Encoder) | |
| 		rw.w.Reset(rw.ResponseWriter) | |
| 		rw.Header().Del("Content-Length") // https://github.com/golang/go/issues/14975 | |
| 		rw.Header().Set("Content-Encoding", rw.encodingName) | |
| 		rw.Header().Add("Vary", "Accept-Encoding") | |
| 	} | |
| 	rw.Header().Del("Accept-Ranges") // we don't know ranges for dynamically-encoded content | |
| } | |
| 
 | |
| // acceptedEncodings returns the list of encodings that the | |
| // client supports, in descending order of preference. If | |
| // the Sec-WebSocket-Key header is present then non-identity | |
| // encodings are not considered. See | |
| // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html. | |
| func acceptedEncodings(r *http.Request) []string { | |
| 	acceptEncHeader := r.Header.Get("Accept-Encoding") | |
| 	websocketKey := r.Header.Get("Sec-WebSocket-Key") | |
| 	if acceptEncHeader == "" { | |
| 		return []string{} | |
| 	} | |
| 
 | |
| 	var prefs []encodingPreference | |
| 
 | |
| 	for _, accepted := range strings.Split(acceptEncHeader, ",") { | |
| 		parts := strings.Split(accepted, ";") | |
| 		encName := strings.ToLower(strings.TrimSpace(parts[0])) | |
| 
 | |
| 		// determine q-factor | |
| 		qFactor := 1.0 | |
| 		if len(parts) > 1 { | |
| 			qFactorStr := strings.ToLower(strings.TrimSpace(parts[1])) | |
| 			if strings.HasPrefix(qFactorStr, "q=") { | |
| 				if qFactorFloat, err := strconv.ParseFloat(qFactorStr[2:], 32); err == nil { | |
| 					if qFactorFloat >= 0 && qFactorFloat <= 1 { | |
| 						qFactor = qFactorFloat | |
| 					} | |
| 				} | |
| 			} | |
| 		} | |
| 
 | |
| 		// encodings with q-factor of 0 are not accepted; | |
| 		// use a small threshold to account for float precision | |
| 		if qFactor < 0.00001 { | |
| 			continue | |
| 		} | |
| 
 | |
| 		// don't encode WebSocket handshakes | |
| 		if websocketKey != "" && encName != "identity" { | |
| 			continue | |
| 		} | |
| 
 | |
| 		prefs = append(prefs, encodingPreference{ | |
| 			encoding: encName, | |
| 			q:        qFactor, | |
| 		}) | |
| 	} | |
| 
 | |
| 	// sort preferences by descending q-factor | |
| 	sort.Slice(prefs, func(i, j int) bool { return prefs[i].q > prefs[j].q }) | |
| 
 | |
| 	// TODO: If no preference, or same pref for all encodings, | |
| 	// and not websocket, use default encoding ordering (enc.Prefer) | |
| 	// for those which are accepted by the client | |
|  | |
| 	prefEncNames := make([]string, len(prefs)) | |
| 	for i := range prefs { | |
| 		prefEncNames[i] = prefs[i].encoding | |
| 	} | |
| 
 | |
| 	return prefEncNames | |
| } | |
| 
 | |
| // encodingPreference pairs an encoding with its q-factor. | |
| type encodingPreference struct { | |
| 	encoding string | |
| 	q        float64 | |
| } | |
| 
 | |
| // Encoder is a type which can encode a stream of data. | |
| type Encoder interface { | |
| 	io.WriteCloser | |
| 	Reset(io.Writer) | |
| } | |
| 
 | |
| // Encoding is a type which can create encoders of its kind | |
| // and return the name used in the Accept-Encoding header. | |
| type Encoding interface { | |
| 	AcceptEncoding() string | |
| 	NewEncoder() Encoder | |
| } | |
| 
 | |
| var bufPool = sync.Pool{ | |
| 	New: func() interface{} { | |
| 		return new(bytes.Buffer) | |
| 	}, | |
| } | |
| 
 | |
| // defaultMinLength is the minimum length at which to compress content. | |
| const defaultMinLength = 512 | |
| 
 | |
| // Interface guards | |
| var ( | |
| 	_ caddy.Provisioner           = (*Encode)(nil) | |
| 	_ caddyhttp.MiddlewareHandler = (*Encode)(nil) | |
| 	_ caddyhttp.HTTPInterfaces    = (*responseWriter)(nil) | |
| )
 | |
| 
 |