mirror of
https://github.com/caddyserver/caddy.git
synced 2024-12-29 15:13:49 +03:00
3c90e370a4
This commit goes a long way toward making automated documentation of Caddy config and Caddy modules possible. It's a broad, sweeping change, but mostly internal. It allows us to automatically generate docs for all Caddy modules (including future third-party ones) and make them viewable on a web page; it also doubles as godoc comments. As such, this commit makes significant progress in migrating the docs from our temporary wiki page toward our new website which is still under construction. With this change, all host modules will use ctx.LoadModule() and pass in both the struct pointer and the field name as a string. This allows the reflect package to read the struct tag from that field so that it can get the necessary information like the module namespace and the inline key. This has the nice side-effect of unifying the code and documentation. It also simplifies module loading, and handles several variations on field types for raw module fields (i.e. variations on json.RawMessage, such as arrays and maps). I also renamed ModuleInfo.Name -> ModuleInfo.ID, to make it clear that the ID is the "full name" which includes both the module namespace and the name. This clarity is helpful when describing module hierarchy. As of this change, Caddy modules are no longer an experimental design. I think the architecture is good enough to go forward.
345 lines
9.8 KiB
Go
345 lines
9.8 KiB
Go
// 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 {
|
|
EncodingsRaw caddy.ModuleMap `json:"encodings,omitempty" caddy:"namespace=http.encoders"`
|
|
Prefer []string `json:"prefer,omitempty"`
|
|
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 theshold 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)
|
|
)
|