mirror of
				https://github.com/caddyserver/caddy.git
				synced 2025-10-31 10:37:24 -04:00 
			
		
		
		
	
		
			Some checks failed
		
		
	
	Tests / test (./cmd/caddy/caddy, ~1.22.3, ubuntu-latest, 0, 1.22, linux) (push) Failing after 2m43s
				
			Tests / test (./cmd/caddy/caddy, ~1.23.0, ubuntu-latest, 0, 1.23, linux) (push) Failing after 2m26s
				
			Tests / test (s390x on IBM Z) (push) Has been skipped
				
			Tests / goreleaser-check (push) Successful in 40s
				
			Cross-Build / build (~1.22.3, 1.22, aix) (push) Successful in 2m55s
				
			Cross-Build / build (~1.22.3, 1.22, dragonfly) (push) Successful in 3m1s
				
			Cross-Build / build (~1.22.3, 1.22, freebsd) (push) Successful in 3m1s
				
			Cross-Build / build (~1.22.3, 1.22, netbsd) (push) Successful in 3m1s
				
			Cross-Build / build (~1.22.3, 1.22, windows) (push) Successful in 3m1s
				
			Cross-Build / build (~1.22.3, 1.22, darwin) (push) Failing after 13m23s
				
			Cross-Build / build (~1.23.0, 1.23, aix) (push) Successful in 2m40s
				
			Cross-Build / build (~1.22.3, 1.22, illumos) (push) Failing after 11m51s
				
			Cross-Build / build (~1.22.3, 1.22, linux) (push) Failing after 11m47s
				
			Cross-Build / build (~1.23.0, 1.23, darwin) (push) Successful in 2m29s
				
			Cross-Build / build (~1.23.0, 1.23, linux) (push) Successful in 2m39s
				
			Cross-Build / build (~1.22.3, 1.22, openbsd) (push) Failing after 13m27s
				
			Cross-Build / build (~1.22.3, 1.22, solaris) (push) Failing after 13m27s
				
			Cross-Build / build (~1.23.0, 1.23, netbsd) (push) Successful in 2m41s
				
			Cross-Build / build (~1.23.0, 1.23, openbsd) (push) Successful in 2m54s
				
			Cross-Build / build (~1.23.0, 1.23, solaris) (push) Successful in 2m40s
				
			Cross-Build / build (~1.23.0, 1.23, dragonfly) (push) Failing after 14m19s
				
			Cross-Build / build (~1.23.0, 1.23, freebsd) (push) Failing after 14m10s
				
			Cross-Build / build (~1.23.0, 1.23, illumos) (push) Failing after 14m2s
				
			Cross-Build / build (~1.23.0, 1.23, windows) (push) Failing after 11m58s
				
			Lint / lint (ubuntu-latest, linux) (push) Failing after 3m44s
				
			Lint / govulncheck (push) Successful in 2m18s
				
			Tests / test (./cmd/caddy/caddy, ~1.22.3, macos-14, 0, 1.22, mac) (push) Has been cancelled
				
			Tests / test (./cmd/caddy/caddy, ~1.23.0, macos-14, 0, 1.23, mac) (push) Has been cancelled
				
			Tests / test (./cmd/caddy/caddy.exe, ~1.22.3, windows-latest, True, 1.22, windows) (push) Has been cancelled
				
			Tests / test (./cmd/caddy/caddy.exe, ~1.23.0, windows-latest, True, 1.23, windows) (push) Has been cancelled
				
			Lint / lint (macos-14, mac) (push) Has been cancelled
				
			Lint / lint (windows-latest, windows) (push) Has been cancelled
				
			* perf: use zap's Check() to prevent useless allocs * fix * fix * fix * fix * restore previous replacer behavior * fix linter
		
			
				
	
	
		
			353 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			353 lines
		
	
	
		
			10 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 intercept
 | |
| 
 | |
| import (
 | |
| 	"bytes"
 | |
| 	"fmt"
 | |
| 	"net/http"
 | |
| 	"strconv"
 | |
| 	"strings"
 | |
| 	"sync"
 | |
| 
 | |
| 	"go.uber.org/zap"
 | |
| 	"go.uber.org/zap/zapcore"
 | |
| 
 | |
| 	"github.com/caddyserver/caddy/v2"
 | |
| 	"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
 | |
| 	"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
 | |
| 	"github.com/caddyserver/caddy/v2/modules/caddyhttp"
 | |
| )
 | |
| 
 | |
| func init() {
 | |
| 	caddy.RegisterModule(Intercept{})
 | |
| 	httpcaddyfile.RegisterHandlerDirective("intercept", parseCaddyfile)
 | |
| }
 | |
| 
 | |
| // Intercept is a middleware that intercepts then replaces or modifies the original response.
 | |
| // It can, for instance, be used to implement X-Sendfile/X-Accel-Redirect-like features
 | |
| // when using modules like FrankenPHP or Caddy Snake.
 | |
| //
 | |
| // EXPERIMENTAL: Subject to change or removal.
 | |
| type Intercept struct {
 | |
| 	// List of handlers and their associated matchers to evaluate
 | |
| 	// after successful response generation.
 | |
| 	// The first handler that matches the original response will
 | |
| 	// be invoked. The original response body will not be
 | |
| 	// written to the client;
 | |
| 	// it is up to the handler to finish handling the response.
 | |
| 	//
 | |
| 	// Three new placeholders are available in this handler chain:
 | |
| 	// - `{http.intercept.status_code}` The status code from the response
 | |
| 	// - `{http.intercept.header.*}` The headers from the response
 | |
| 	HandleResponse []caddyhttp.ResponseHandler `json:"handle_response,omitempty"`
 | |
| 
 | |
| 	// Holds the named response matchers from the Caddyfile while adapting
 | |
| 	responseMatchers map[string]caddyhttp.ResponseMatcher
 | |
| 
 | |
| 	// Holds the handle_response Caddyfile tokens while adapting
 | |
| 	handleResponseSegments []*caddyfile.Dispenser
 | |
| 
 | |
| 	logger *zap.Logger
 | |
| }
 | |
| 
 | |
| // CaddyModule returns the Caddy module information.
 | |
| //
 | |
| // EXPERIMENTAL: Subject to change or removal.
 | |
| func (Intercept) CaddyModule() caddy.ModuleInfo {
 | |
| 	return caddy.ModuleInfo{
 | |
| 		ID:  "http.handlers.intercept",
 | |
| 		New: func() caddy.Module { return new(Intercept) },
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // Provision ensures that i is set up properly before use.
 | |
| //
 | |
| // EXPERIMENTAL: Subject to change or removal.
 | |
| func (irh *Intercept) Provision(ctx caddy.Context) error {
 | |
| 	// set up any response routes
 | |
| 	for i, rh := range irh.HandleResponse {
 | |
| 		err := rh.Provision(ctx)
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("provisioning response handler %d: %w", i, err)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	irh.logger = ctx.Logger()
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| var bufPool = sync.Pool{
 | |
| 	New: func() any {
 | |
| 		return new(bytes.Buffer)
 | |
| 	},
 | |
| }
 | |
| 
 | |
| // TODO: handle status code replacement
 | |
| //
 | |
| // EXPERIMENTAL: Subject to change or removal.
 | |
| type interceptedResponseHandler struct {
 | |
| 	caddyhttp.ResponseRecorder
 | |
| 	replacer     *caddy.Replacer
 | |
| 	handler      caddyhttp.ResponseHandler
 | |
| 	handlerIndex int
 | |
| 	statusCode   int
 | |
| }
 | |
| 
 | |
| // EXPERIMENTAL: Subject to change or removal.
 | |
| func (irh interceptedResponseHandler) WriteHeader(statusCode int) {
 | |
| 	if irh.statusCode != 0 && (statusCode < 100 || statusCode >= 200) {
 | |
| 		irh.ResponseRecorder.WriteHeader(irh.statusCode)
 | |
| 
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	irh.ResponseRecorder.WriteHeader(statusCode)
 | |
| }
 | |
| 
 | |
| // EXPERIMENTAL: Subject to change or removal.
 | |
| func (ir Intercept) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
 | |
| 	buf := bufPool.Get().(*bytes.Buffer)
 | |
| 	buf.Reset()
 | |
| 	defer bufPool.Put(buf)
 | |
| 
 | |
| 	repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
 | |
| 	rec := interceptedResponseHandler{replacer: repl}
 | |
| 	rec.ResponseRecorder = caddyhttp.NewResponseRecorder(w, buf, func(status int, header http.Header) bool {
 | |
| 		// see if any response handler is configured for this original response
 | |
| 		for i, rh := range ir.HandleResponse {
 | |
| 			if rh.Match != nil && !rh.Match.Match(status, header) {
 | |
| 				continue
 | |
| 			}
 | |
| 			rec.handler = rh
 | |
| 			rec.handlerIndex = i
 | |
| 
 | |
| 			// if configured to only change the status code,
 | |
| 			// do that then stream
 | |
| 			if statusCodeStr := rh.StatusCode.String(); statusCodeStr != "" {
 | |
| 				sc, err := strconv.Atoi(repl.ReplaceAll(statusCodeStr, ""))
 | |
| 				if err != nil {
 | |
| 					rec.statusCode = http.StatusInternalServerError
 | |
| 				} else {
 | |
| 					rec.statusCode = sc
 | |
| 				}
 | |
| 			}
 | |
| 
 | |
| 			return rec.statusCode == 0
 | |
| 		}
 | |
| 
 | |
| 		return false
 | |
| 	})
 | |
| 
 | |
| 	if err := next.ServeHTTP(rec, r); err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	if !rec.Buffered() {
 | |
| 		return nil
 | |
| 	}
 | |
| 
 | |
| 	// set up the replacer so that parts of the original response can be
 | |
| 	// used for routing decisions
 | |
| 	for field, value := range rec.Header() {
 | |
| 		repl.Set("http.intercept.header."+field, strings.Join(value, ","))
 | |
| 	}
 | |
| 	repl.Set("http.intercept.status_code", rec.Status())
 | |
| 
 | |
| 	if c := ir.logger.Check(zapcore.DebugLevel, "handling response"); c != nil {
 | |
| 		c.Write(zap.Int("handler", rec.handlerIndex))
 | |
| 	}
 | |
| 
 | |
| 	// pass the request through the response handler routes
 | |
| 	return rec.handler.Routes.Compile(next).ServeHTTP(w, r)
 | |
| }
 | |
| 
 | |
| // UnmarshalCaddyfile sets up the handler from Caddyfile tokens. Syntax:
 | |
| //
 | |
| //	intercept [<matcher>] {
 | |
| //	    # intercept original responses
 | |
| //	    @name {
 | |
| //	        status <code...>
 | |
| //	        header <field> [<value>]
 | |
| //	    }
 | |
| //	    replace_status [<matcher>] <status_code>
 | |
| //	    handle_response [<matcher>] {
 | |
| //	        <directives...>
 | |
| //	    }
 | |
| //	}
 | |
| //
 | |
| // The FinalizeUnmarshalCaddyfile method should be called after this
 | |
| // to finalize parsing of "handle_response" blocks, if possible.
 | |
| //
 | |
| // EXPERIMENTAL: Subject to change or removal.
 | |
| func (i *Intercept) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
 | |
| 	// collect the response matchers defined as subdirectives
 | |
| 	// prefixed with "@" for use with "handle_response" blocks
 | |
| 	i.responseMatchers = make(map[string]caddyhttp.ResponseMatcher)
 | |
| 
 | |
| 	d.Next() // consume the directive name
 | |
| 	for d.NextBlock(0) {
 | |
| 		// if the subdirective has an "@" prefix then we
 | |
| 		// parse it as a response matcher for use with "handle_response"
 | |
| 		if strings.HasPrefix(d.Val(), matcherPrefix) {
 | |
| 			err := caddyhttp.ParseNamedResponseMatcher(d.NewFromNextSegment(), i.responseMatchers)
 | |
| 			if err != nil {
 | |
| 				return err
 | |
| 			}
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		switch d.Val() {
 | |
| 		case "handle_response":
 | |
| 			// delegate the parsing of handle_response to the caller,
 | |
| 			// since we need the httpcaddyfile.Helper to parse subroutes.
 | |
| 			// See h.FinalizeUnmarshalCaddyfile
 | |
| 			i.handleResponseSegments = append(i.handleResponseSegments, d.NewFromNextSegment())
 | |
| 
 | |
| 		case "replace_status":
 | |
| 			args := d.RemainingArgs()
 | |
| 			if len(args) != 1 && len(args) != 2 {
 | |
| 				return d.Errf("must have one or two arguments: an optional response matcher, and a status code")
 | |
| 			}
 | |
| 
 | |
| 			responseHandler := caddyhttp.ResponseHandler{}
 | |
| 
 | |
| 			if len(args) == 2 {
 | |
| 				if !strings.HasPrefix(args[0], matcherPrefix) {
 | |
| 					return d.Errf("must use a named response matcher, starting with '@'")
 | |
| 				}
 | |
| 				foundMatcher, ok := i.responseMatchers[args[0]]
 | |
| 				if !ok {
 | |
| 					return d.Errf("no named response matcher defined with name '%s'", args[0][1:])
 | |
| 				}
 | |
| 				responseHandler.Match = &foundMatcher
 | |
| 				responseHandler.StatusCode = caddyhttp.WeakString(args[1])
 | |
| 			} else if len(args) == 1 {
 | |
| 				responseHandler.StatusCode = caddyhttp.WeakString(args[0])
 | |
| 			}
 | |
| 
 | |
| 			// make sure there's no block, cause it doesn't make sense
 | |
| 			if nesting := d.Nesting(); d.NextBlock(nesting) {
 | |
| 				return d.Errf("cannot define routes for 'replace_status', use 'handle_response' instead.")
 | |
| 			}
 | |
| 
 | |
| 			i.HandleResponse = append(
 | |
| 				i.HandleResponse,
 | |
| 				responseHandler,
 | |
| 			)
 | |
| 
 | |
| 		default:
 | |
| 			return d.Errf("unrecognized subdirective %s", d.Val())
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // FinalizeUnmarshalCaddyfile finalizes the Caddyfile parsing which
 | |
| // requires having an httpcaddyfile.Helper to function, to parse subroutes.
 | |
| //
 | |
| // EXPERIMENTAL: Subject to change or removal.
 | |
| func (i *Intercept) FinalizeUnmarshalCaddyfile(helper httpcaddyfile.Helper) error {
 | |
| 	for _, d := range i.handleResponseSegments {
 | |
| 		// consume the "handle_response" token
 | |
| 		d.Next()
 | |
| 		args := d.RemainingArgs()
 | |
| 
 | |
| 		// TODO: Remove this check at some point in the future
 | |
| 		if len(args) == 2 {
 | |
| 			return d.Errf("configuring 'handle_response' for status code replacement is no longer supported. Use 'replace_status' instead.")
 | |
| 		}
 | |
| 
 | |
| 		if len(args) > 1 {
 | |
| 			return d.Errf("too many arguments for 'handle_response': %s", args)
 | |
| 		}
 | |
| 
 | |
| 		var matcher *caddyhttp.ResponseMatcher
 | |
| 		if len(args) == 1 {
 | |
| 			// the first arg should always be a matcher.
 | |
| 			if !strings.HasPrefix(args[0], matcherPrefix) {
 | |
| 				return d.Errf("must use a named response matcher, starting with '@'")
 | |
| 			}
 | |
| 
 | |
| 			foundMatcher, ok := i.responseMatchers[args[0]]
 | |
| 			if !ok {
 | |
| 				return d.Errf("no named response matcher defined with name '%s'", args[0][1:])
 | |
| 			}
 | |
| 			matcher = &foundMatcher
 | |
| 		}
 | |
| 
 | |
| 		// parse the block as routes
 | |
| 		handler, err := httpcaddyfile.ParseSegmentAsSubroute(helper.WithDispenser(d.NewFromNextSegment()))
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 		subroute, ok := handler.(*caddyhttp.Subroute)
 | |
| 		if !ok {
 | |
| 			return helper.Errf("segment was not parsed as a subroute")
 | |
| 		}
 | |
| 		i.HandleResponse = append(
 | |
| 			i.HandleResponse,
 | |
| 			caddyhttp.ResponseHandler{
 | |
| 				Match:  matcher,
 | |
| 				Routes: subroute.Routes,
 | |
| 			},
 | |
| 		)
 | |
| 	}
 | |
| 
 | |
| 	// move the handle_response entries without a matcher to the end.
 | |
| 	// we can't use sort.SliceStable because it will reorder the rest of the
 | |
| 	// entries which may be undesirable because we don't have a good
 | |
| 	// heuristic to use for sorting.
 | |
| 	withoutMatchers := []caddyhttp.ResponseHandler{}
 | |
| 	withMatchers := []caddyhttp.ResponseHandler{}
 | |
| 	for _, hr := range i.HandleResponse {
 | |
| 		if hr.Match == nil {
 | |
| 			withoutMatchers = append(withoutMatchers, hr)
 | |
| 		} else {
 | |
| 			withMatchers = append(withMatchers, hr)
 | |
| 		}
 | |
| 	}
 | |
| 	i.HandleResponse = append(withMatchers, withoutMatchers...)
 | |
| 
 | |
| 	// clean up the bits we only needed for adapting
 | |
| 	i.handleResponseSegments = nil
 | |
| 	i.responseMatchers = nil
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| const matcherPrefix = "@"
 | |
| 
 | |
| func parseCaddyfile(helper httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
 | |
| 	var ir Intercept
 | |
| 	if err := ir.UnmarshalCaddyfile(helper.Dispenser); err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	if err := ir.FinalizeUnmarshalCaddyfile(helper); err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	return ir, nil
 | |
| }
 | |
| 
 | |
| // Interface guards
 | |
| var (
 | |
| 	_ caddy.Provisioner           = (*Intercept)(nil)
 | |
| 	_ caddyfile.Unmarshaler       = (*Intercept)(nil)
 | |
| 	_ caddyhttp.MiddlewareHandler = (*Intercept)(nil)
 | |
| )
 |