@marcel91200 / caddy Mirror · read-only / commits / e73b1173
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 14, 05:15 UTC

reverse_proxy: Ability to mutate headers; set upstream placeholders

Matthew Holt committed Sep 14, 2019 at 13:25 UTC e73b117332a5606a986ee7e03e6f4b0cea7871c9
3 files changed +74 -12
modules/caddyhttp/reverseproxy/healthchecks.go
+3 -3
@@ -111,10 +111,10 @@ func (h *Handler) doActiveHealthChecksForAllHosts() {
111 if network == "unix" || network == "unixgram" || network == "unixpacket" {
112 // this will be used as the Host portion of a http.Request URL, and
113 // paths to socket files would produce an error when creating URL,
114 - // so use a fake Host value instead
115 - hostAddr = network
114 + // so use a fake Host value instead; unix sockets are usually local
115 + hostAddr = "localhost"
116 }
117 - err = h.doActiveHealthCheck(DialInfo{network, addrs[0]}, hostAddr, host)
117 + err = h.doActiveHealthCheck(NewDialInfo(network, addrs[0]), hostAddr, host)
118 if err != nil {
119 log.Printf("[ERROR] reverse_proxy: active health check for host %s: %v", networkAddr, err)
120 }
modules/caddyhttp/reverseproxy/hosts.go
+23
@@ -16,6 +16,8 @@ package reverseproxy
16
17 import (
18 "fmt"
19 + "net"
20 + "strings"
21 "sync/atomic"
22
23 "github.com/caddyserver/caddy/v2"
@@ -173,6 +175,27 @@ type DialInfo struct {
175 // The address to dial. Follows the same
176 // semantics and rules as net.Dial.
177 Address string
178 +
179 + // Host and Port are components of Address,
180 + // pre-split for convenience.
181 + Host, Port string
182 +}
183 +
184 +// NewDialInfo creates and populates a DialInfo
185 +// for the given network and address. It splits
186 +// the address into host and port values if the
187 +// network type supports them, or uses the whole
188 +// address as the port if splitting fails.
189 +func NewDialInfo(network, address string) DialInfo {
190 + var addrHost, addrPort string
191 + if !strings.Contains(network, "unix") {
192 + var err error
193 + addrHost, addrPort, err = net.SplitHostPort(address)
194 + if err != nil {
195 + addrHost = address // assume there was no port
196 + }
197 + }
198 + return DialInfo{network, address, addrHost, addrPort}
199 }
200
201 // String returns the Caddy network address form
modules/caddyhttp/reverseproxy/reverseproxy.go
+48 -9
@@ -21,11 +21,13 @@ import (
21 "net"
22 "net/http"
23 "regexp"
24 + "strconv"
25 "strings"
26 "time"
27
28 "github.com/caddyserver/caddy/v2"
29 "github.com/caddyserver/caddy/v2/modules/caddyhttp"
30 + "github.com/caddyserver/caddy/v2/modules/caddyhttp/headers"
31 "golang.org/x/net/http/httpguts"
32 )
33
@@ -35,12 +37,13 @@ func init() {
37
38 // Handler implements a highly configurable and production-ready reverse proxy.
39 type Handler struct {
38 - TransportRaw json.RawMessage `json:"transport,omitempty"`
39 - CBRaw json.RawMessage `json:"circuit_breaker,omitempty"`
40 - LoadBalancing *LoadBalancing `json:"load_balancing,omitempty"`
41 - HealthChecks *HealthChecks `json:"health_checks,omitempty"`
42 - Upstreams UpstreamPool `json:"upstreams,omitempty"`
43 - FlushInterval caddy.Duration `json:"flush_interval,omitempty"`
40 + TransportRaw json.RawMessage `json:"transport,omitempty"`
41 + CBRaw json.RawMessage `json:"circuit_breaker,omitempty"`
42 + LoadBalancing *LoadBalancing `json:"load_balancing,omitempty"`
43 + HealthChecks *HealthChecks `json:"health_checks,omitempty"`
44 + Upstreams UpstreamPool `json:"upstreams,omitempty"`
45 + FlushInterval caddy.Duration `json:"flush_interval,omitempty"`
46 + Headers *headers.Handler `json:"headers,omitempty"`
47
48 Transport http.RoundTripper `json:"-"`
49 CB CircuitBreaker `json:"-"`
@@ -178,7 +181,7 @@ func (h *Handler) Provision(ctx caddy.Context) error {
181 // make a new upstream based on the original
182 // that has a singular dial address
183 upstreamCopy := *upstream
181 - upstreamCopy.dialInfo = DialInfo{network, addr}
184 + upstreamCopy.dialInfo = NewDialInfo(network, addr)
185 upstreamCopy.Dial = upstreamCopy.dialInfo.String()
186 upstreamCopy.cb = h.CB
187
@@ -187,7 +190,7 @@ func (h *Handler) Provision(ctx caddy.Context) error {
190 // TODO: make hosts modular, so that their state can be distributed in enterprise for example
191 // TODO: If distributed, the pool should be stored in storage...
192 var host Host = new(upstreamHost)
190 - activeHost, loaded := hosts.LoadOrStore(upstreamCopy.Dial, host)
193 + activeHost, loaded := hosts.LoadOrStore(upstreamCopy.dialInfo.String(), host)
194 if loaded {
195 host = activeHost.(Host)
196 }
@@ -243,6 +246,8 @@ func (h *Handler) Cleanup() error {
246 }
247
248 func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
249 + repl := r.Context().Value(caddy.ReplacerCtxKey).(caddy.Replacer)
250 +
251 // prepare the request for proxying; this is needed only once
252 err := h.prepareRequest(r)
253 if err != nil {
@@ -250,6 +255,11 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht
255 fmt.Errorf("preparing request for upstream round-trip: %v", err))
256 }
257
258 + // we will need the original headers and Host
259 + // value if header operations are configured
260 + reqHeader := r.Header
261 + reqHost := r.Host
262 +
263 start := time.Now()
264
265 var proxyErr error
@@ -258,7 +268,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht
268 upstream := h.LoadBalancing.SelectionPolicy.Select(h.Upstreams, r)
269 if upstream == nil {
270 if proxyErr == nil {
261 - proxyErr = fmt.Errorf("no available upstreams")
271 + proxyErr = fmt.Errorf("no upstreams available")
272 }
273 if !h.tryAgain(start, proxyErr) {
274 break
@@ -272,6 +282,26 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht
282 ctx := context.WithValue(r.Context(), DialInfoCtxKey, upstream.dialInfo)
283 r = r.WithContext(ctx)
284
285 + // set placeholders with information about this upstream
286 + repl.Set("http.handlers.reverse_proxy.upstream.address", upstream.dialInfo.String())
287 + repl.Set("http.handlers.reverse_proxy.upstream.hostport", upstream.dialInfo.Address)
288 + repl.Set("http.handlers.reverse_proxy.upstream.host", upstream.dialInfo.Host)
289 + repl.Set("http.handlers.reverse_proxy.upstream.port", upstream.dialInfo.Port)
290 + repl.Set("http.handlers.reverse_proxy.upstream.requests", strconv.Itoa(upstream.Host.NumRequests()))
291 + repl.Set("http.handlers.reverse_proxy.upstream.max_requests", strconv.Itoa(upstream.MaxRequests))
292 + repl.Set("http.handlers.reverse_proxy.upstream.fails", strconv.Itoa(upstream.Host.Fails()))
293 +
294 + // mutate request headers according to this upstream;
295 + // because we're in a retry loop, we have to copy
296 + // headers (and the r.Host value) from the original
297 + // so that each retry is identical to the first
298 + if h.Headers != nil && h.Headers.Request != nil {
299 + r.Header = make(http.Header)
300 + copyHeader(r.Header, reqHeader)
301 + r.Host = reqHost
302 + h.Headers.Request.ApplyToRequest(r)
303 + }
304 +
305 // proxy the request to that upstream
306 proxyErr = h.reverseProxy(w, r, upstream)
307 if proxyErr == nil || proxyErr == context.Canceled {
@@ -428,6 +458,15 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, upstre
458 rw.Header().Add("Trailer", strings.Join(trailerKeys, ", "))
459 }
460
461 + // apply any response header operations
462 + if h.Headers != nil && h.Headers.Response != nil {
463 + if h.Headers.Response.Require == nil ||
464 + h.Headers.Response.Require.Match(res.StatusCode, rw.Header()) {
465 + repl := req.Context().Value(caddy.ReplacerCtxKey).(caddy.Replacer)
466 + h.Headers.Response.ApplyTo(rw.Header(), repl)
467 + }
468 + }
469 +
470 rw.WriteHeader(res.StatusCode)
471
472 err = h.copyResponse(rw, res.Body, h.flushInterval(req, res))