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))