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: Allow dynamic backends (closes #990 and #1539)
This PR enables the use of placeholders in an upstream's Dial address. A Dial address must represent precisely one socket after replacements. See also #998 and #1639.
Matthew Holt committed
Oct 11, 2019 at 14:25 UTC
1e31be8de0d1d5587348619225456a793cb30f7f
6 files changed
+112
-134
listeners.go
+4
-3
@@ -286,9 +286,10 @@ func JoinNetworkAddress(network, host, port string) string {
286
if network != "" {
287
a = network + "/"
288
}
289
- a += host
290
- if port != "" {
291
- a += ":" + port
289
+ if host != "" && port == "" {
290
+ a += host
291
+ } else if port != "" {
292
+ a += net.JoinHostPort(host, port)
293
}
294
return a
295
}
listeners_test.go
+4
@@ -138,6 +138,10 @@ func TestJoinNetworkAddress(t *testing.T) {
138
network: "unix", host: "/foo/bar", port: "",
139
expect: "unix//foo/bar",
140
},
141
+ {
142
+ network: "", host: "::1", port: "1234",
143
+ expect: "[::1]:1234",
144
+ },
145
} {
146
actual := JoinNetworkAddress(tc.network, tc.host, tc.port)
147
if actual != tc.expect {
modules/caddyhttp/reverseproxy/caddyfile.go
+4
-6
@@ -81,9 +81,7 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
81
func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
82
for d.Next() {
83
for _, up := range d.RemainingArgs() {
84
- h.Upstreams = append(h.Upstreams, &Upstream{
85
- Dial: up,
86
- })
84
+ h.Upstreams = append(h.Upstreams, &Upstream{Dial: up})
85
}
86
87
for d.NextBlock(0) {
@@ -94,9 +92,7 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
92
return d.ArgErr()
93
}
94
for _, up := range args {
97
- h.Upstreams = append(h.Upstreams, &Upstream{
98
- Dial: up,
99
- })
95
+ h.Upstreams = append(h.Upstreams, &Upstream{Dial: up})
96
}
97
98
case "lb_policy":
@@ -502,6 +498,7 @@ func (h *HTTPTransport) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
498
if d.Val() == "off" {
499
var disable bool
500
h.KeepAlive.Enabled = &disable
501
+ break
502
}
503
dur, err := time.ParseDuration(d.Val())
504
if err != nil {
@@ -521,6 +518,7 @@ func (h *HTTPTransport) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
518
h.KeepAlive = new(KeepAlive)
519
}
520
h.KeepAlive.MaxIdleConns = num
521
+ h.KeepAlive.MaxIdleConnsPerHost = num
522
523
default:
524
return d.Errf("unrecognized subdirective %s", d.Val())
modules/caddyhttp/reverseproxy/healthchecks.go
+3
-3
@@ -115,7 +115,7 @@ func (h *Handler) doActiveHealthChecksForAllHosts() {
115
// so use a fake Host value instead; unix sockets are usually local
116
hostAddr = "localhost"
117
}
118
- err = h.doActiveHealthCheck(NewDialInfo(network, addrs[0]), hostAddr, host)
118
+ err = h.doActiveHealthCheck(DialInfo{Network: network, Address: addrs[0]}, hostAddr, host)
119
if err != nil {
120
log.Printf("[ERROR] reverse_proxy: active health check for host %s: %v", networkAddr, err)
121
}
@@ -259,7 +259,7 @@ func (h *Handler) countFailure(upstream *Upstream) {
259
err := upstream.Host.CountFail(1)
260
if err != nil {
261
log.Printf("[ERROR] proxy: upstream %s: counting failure: %v",
262
- upstream.dialInfo, err)
262
+ upstream.Dial, err)
263
}
264
265
// forget it later
@@ -268,7 +268,7 @@ func (h *Handler) countFailure(upstream *Upstream) {
268
err := host.CountFail(-1)
269
if err != nil {
270
log.Printf("[ERROR] proxy: upstream %s: expiring failure: %v",
271
- upstream.dialInfo, err)
271
+ upstream.Dial, err)
272
}
273
}(upstream.Host, failDuration)
274
}
modules/caddyhttp/reverseproxy/hosts.go
+37
-25
@@ -72,7 +72,6 @@ type Upstream struct {
72
73
healthCheckPolicy *PassiveHealthChecks
74
cb CircuitBreaker
75
- dialInfo DialInfo
75
}
76
77
// Available returns true if the remote host
@@ -149,8 +148,7 @@ func (uh *upstreamHost) CountFail(delta int) error {
148
}
149
150
// SetHealthy sets the upstream has healthy or unhealthy
152
-// and returns true if the value was different from before,
153
-// or an error if the adjustment failed.
151
+// and returns true if the new value is different.
152
func (uh *upstreamHost) SetHealthy(healthy bool) (bool, error) {
153
var unhealthy, compare int32 = 1, 0
154
if healthy {
@@ -167,8 +165,12 @@ func (uh *upstreamHost) SetHealthy(healthy bool) (bool, error) {
165
// a host that can be represented in a URL, but
166
// they certainly have a network name and address).
167
type DialInfo struct {
170
- // The network to use. This should be one of the
171
- // values that is accepted by net.Dial:
168
+ // Upstream is the Upstream associated with
169
+ // this DialInfo. It may be nil.
170
+ Upstream *Upstream
171
+
172
+ // The network to use. This should be one of
173
+ // the values that is accepted by net.Dial:
174
// https://golang.org/pkg/net/#Dial
175
Network string
176
@@ -176,33 +178,43 @@ type DialInfo struct {
178
// semantics and rules as net.Dial.
179
Address string
180
179
- // Host and Port are components of Address,
180
- // pre-split for convenience.
181
+ // Host and Port are components of Address.
182
Host, Port string
183
}
184
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
-
185
// String returns the Caddy network address form
186
// by joining the network and address with a
187
// forward slash.
188
func (di DialInfo) String() string {
205
- return di.Network + "/" + di.Address
189
+ return caddy.JoinNetworkAddress(di.Network, di.Host, di.Port)
190
+}
191
+
192
+// fillDialInfo returns a filled DialInfo for the given upstream, using
193
+// the given Replacer. Note that the returned value is not a pointer.
194
+func fillDialInfo(upstream *Upstream, repl caddy.Replacer) (DialInfo, error) {
195
+ dial := repl.ReplaceAll(upstream.Dial, "")
196
+ netw, addrs, err := caddy.ParseNetworkAddress(dial)
197
+ if err != nil {
198
+ return DialInfo{}, fmt.Errorf("upstream %s: invalid dial address %s: %v", upstream.Dial, dial, err)
199
+ }
200
+ if len(addrs) != 1 {
201
+ return DialInfo{}, fmt.Errorf("upstream %s: dial address must represent precisely one socket: %s represents %d",
202
+ upstream.Dial, dial, len(addrs))
203
+ }
204
+ var dialHost, dialPort string
205
+ if !strings.Contains(netw, "unix") {
206
+ dialHost, dialPort, err = net.SplitHostPort(addrs[0])
207
+ if err != nil {
208
+ dialHost = addrs[0] // assume there was no port
209
+ }
210
+ }
211
+ return DialInfo{
212
+ Upstream: upstream,
213
+ Network: netw,
214
+ Address: addrs[0],
215
+ Host: dialHost,
216
+ Port: dialPort,
217
+ }, nil
218
}
219
220
// DialInfoCtxKey is used to store a DialInfo
modules/caddyhttp/reverseproxy/reverseproxy.go
+60
-97
@@ -87,6 +87,7 @@ func (h *Handler) Provision(ctx caddy.Context) error {
87
h.CBRaw = nil // allow GC to deallocate
88
}
89
90
+ // set up transport
91
if h.Transport == nil {
92
t := &HTTPTransport{
93
KeepAlive: &KeepAlive{
@@ -102,6 +103,7 @@ func (h *Handler) Provision(ctx caddy.Context) error {
103
h.Transport = t
104
}
105
106
+ // set up load balancing
107
if h.LoadBalancing == nil {
108
h.LoadBalancing = new(LoadBalancing)
109
}
@@ -152,85 +154,40 @@ func (h *Handler) Provision(ctx caddy.Context) error {
154
go h.activeHealthChecker()
155
}
156
155
- var allUpstreams []*Upstream
157
+ // set up upstreams
158
for _, upstream := range h.Upstreams {
157
- // if a port was not specified (and the network type uses
158
- // ports), then maybe we can figure out the default port
159
- netw, host, port, err := caddy.SplitNetworkAddress(upstream.Dial)
160
- if err != nil && port == "" && !strings.Contains(netw, "unix") {
161
- if host == "" {
162
- // assume all that was given was the host, no port
163
- host = upstream.Dial
164
- }
165
- // a port was not specified, but we may be able to
166
- // infer it if we know the standard ports on which
167
- // the transport protocol operates
168
- if ht, ok := h.Transport.(*HTTPTransport); ok {
169
- defaultPort := "80"
170
- if ht.TLS != nil {
171
- defaultPort = "443"
172
- }
173
- upstream.Dial = caddy.JoinNetworkAddress(netw, host, defaultPort)
174
- }
159
+ // create or get the host representation for this upstream
160
+ var host Host = new(upstreamHost)
161
+ existingHost, loaded := hosts.LoadOrStore(upstream.Dial, host)
162
+ if loaded {
163
+ host = existingHost.(Host)
164
}
165
+ upstream.Host = host
166
177
- // upstreams are allowed to map to only a single host,
178
- // but an upstream's address may semantically represent
179
- // multiple addresses, so make sure to handle each
180
- // one in turn based on this one upstream config
181
- network, addresses, err := caddy.ParseNetworkAddress(upstream.Dial)
182
- if err != nil {
183
- return fmt.Errorf("parsing dial address: %v", err)
184
- }
185
-
186
- for _, addr := range addresses {
187
- // make a new upstream based on the original
188
- // that has a singular dial address
189
- upstreamCopy := *upstream
190
- upstreamCopy.dialInfo = NewDialInfo(network, addr)
191
- upstreamCopy.Dial = upstreamCopy.dialInfo.String()
192
- upstreamCopy.cb = h.CB
193
-
194
- // if host already exists from a current config,
195
- // use that instead; otherwise, add it
196
- // TODO: make hosts modular, so that their state can be distributed in enterprise for example
197
- // TODO: If distributed, the pool should be stored in storage...
198
- var host Host = new(upstreamHost)
199
- activeHost, loaded := hosts.LoadOrStore(upstreamCopy.dialInfo.String(), host)
200
- if loaded {
201
- host = activeHost.(Host)
202
- }
203
- upstreamCopy.Host = host
204
-
205
- // if the passive health checker has a non-zero "unhealthy
206
- // request count" but the upstream has no MaxRequests set
207
- // (they are the same thing, but one is a default value for
208
- // for upstreams with a zero MaxRequests), copy the default
209
- // value into this upstream, since the value in the upstream
210
- // (MaxRequests) is what is used during availability checks
211
- if h.HealthChecks != nil &&
212
- h.HealthChecks.Passive != nil &&
213
- h.HealthChecks.Passive.UnhealthyRequestCount > 0 &&
214
- upstreamCopy.MaxRequests == 0 {
215
- upstreamCopy.MaxRequests = h.HealthChecks.Passive.UnhealthyRequestCount
216
- }
167
+ // give it the circuit breaker, if any
168
+ upstream.cb = h.CB
169
218
- // upstreams need independent access to the passive
219
- // health check policy because they run outside of the
220
- // scope of a request handler
221
- if h.HealthChecks != nil {
222
- upstreamCopy.healthCheckPolicy = h.HealthChecks.Passive
223
- }
170
+ // if the passive health checker has a non-zero UnhealthyRequestCount
171
+ // but the upstream has no MaxRequests set (they are the same thing,
172
+ // but the passive health checker is a default value for for upstreams
173
+ // without MaxRequests), copy the value into this upstream, since the
174
+ // value in the upstream (MaxRequests) is what is used during
175
+ // availability checks
176
+ if h.HealthChecks != nil &&
177
+ h.HealthChecks.Passive != nil &&
178
+ h.HealthChecks.Passive.UnhealthyRequestCount > 0 &&
179
+ upstream.MaxRequests == 0 {
180
+ upstream.MaxRequests = h.HealthChecks.Passive.UnhealthyRequestCount
181
+ }
182
225
- allUpstreams = append(allUpstreams, &upstreamCopy)
183
+ // upstreams need independent access to the passive
184
+ // health check policy because passive health checks
185
+ // run without access to h.
186
+ if h.HealthChecks != nil {
187
+ upstream.healthCheckPolicy = h.HealthChecks.Passive
188
}
189
}
190
229
- // replace the unmarshaled upstreams (possible 1:many
230
- // address mapping) with our list, which is mapped 1:1,
231
- // thus may have expanded the original list
232
- h.Upstreams = allUpstreams
233
-
191
return nil
192
}
193
@@ -247,7 +204,7 @@ func (h *Handler) Cleanup() error {
204
205
// remove hosts from our config from the pool
206
for _, upstream := range h.Upstreams {
250
- hosts.Delete(upstream.dialInfo.String())
207
+ hosts.Delete(upstream.Dial)
208
}
209
210
return nil
@@ -284,17 +241,25 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht
241
continue
242
}
243
244
+ // the dial address may vary per-request if placeholders are
245
+ // used, so perform those replacements here; the resulting
246
+ // DialInfo struct should have valid network address syntax
247
+ dialInfo, err := fillDialInfo(upstream, repl)
248
+ if err != nil {
249
+ return fmt.Errorf("making dial info: %v", err)
250
+ }
251
+
252
// attach to the request information about how to dial the upstream;
253
// this is necessary because the information cannot be sufficiently
254
// or satisfactorily represented in a URL
290
- ctx := context.WithValue(r.Context(), DialInfoCtxKey, upstream.dialInfo)
255
+ ctx := context.WithValue(r.Context(), DialInfoCtxKey, dialInfo)
256
r = r.WithContext(ctx)
257
258
// set placeholders with information about this upstream
294
- repl.Set("http.handlers.reverse_proxy.upstream.address", upstream.dialInfo.String())
295
- repl.Set("http.handlers.reverse_proxy.upstream.hostport", upstream.dialInfo.Address)
296
- repl.Set("http.handlers.reverse_proxy.upstream.host", upstream.dialInfo.Host)
297
- repl.Set("http.handlers.reverse_proxy.upstream.port", upstream.dialInfo.Port)
259
+ repl.Set("http.handlers.reverse_proxy.upstream.address", dialInfo.String())
260
+ repl.Set("http.handlers.reverse_proxy.upstream.hostport", dialInfo.Address)
261
+ repl.Set("http.handlers.reverse_proxy.upstream.host", dialInfo.Host)
262
+ repl.Set("http.handlers.reverse_proxy.upstream.port", dialInfo.Port)
263
repl.Set("http.handlers.reverse_proxy.upstream.requests", strconv.Itoa(upstream.Host.NumRequests()))
264
repl.Set("http.handlers.reverse_proxy.upstream.max_requests", strconv.Itoa(upstream.MaxRequests))
265
repl.Set("http.handlers.reverse_proxy.upstream.fails", strconv.Itoa(upstream.Host.Fails()))
@@ -311,7 +276,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht
276
}
277
278
// proxy the request to that upstream
314
- proxyErr = h.reverseProxy(w, r, upstream)
279
+ proxyErr = h.reverseProxy(w, r, dialInfo)
280
if proxyErr == nil || proxyErr == context.Canceled {
281
// context.Canceled happens when the downstream client
282
// cancels the request, which is not our failure
@@ -405,12 +370,12 @@ func (h Handler) prepareRequest(req *http.Request) error {
370
// reverseProxy performs a round-trip to the given backend and processes the response with the client.
371
// (This method is mostly the beginning of what was borrowed from the net/http/httputil package in the
372
// Go standard library which was used as the foundation.)
408
-func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, upstream *Upstream) error {
409
- upstream.Host.CountRequest(1)
410
- defer upstream.Host.CountRequest(-1)
373
+func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, di DialInfo) error {
374
+ di.Upstream.Host.CountRequest(1)
375
+ defer di.Upstream.Host.CountRequest(-1)
376
377
// point the request to this upstream
413
- h.directRequest(req, upstream)
378
+ h.directRequest(req, di)
379
380
// do the round-trip
381
start := time.Now()
@@ -421,8 +386,8 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, upstre
386
}
387
388
// update circuit breaker on current conditions
424
- if upstream.cb != nil {
425
- upstream.cb.RecordMetric(res.StatusCode, latency)
389
+ if di.Upstream.cb != nil {
390
+ di.Upstream.cb.RecordMetric(res.StatusCode, latency)
391
}
392
393
// perform passive health checks (if enabled)
@@ -430,14 +395,14 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, upstre
395
// strike if the status code matches one that is "bad"
396
for _, badStatus := range h.HealthChecks.Passive.UnhealthyStatus {
397
if caddyhttp.StatusCodeMatches(res.StatusCode, badStatus) {
433
- h.countFailure(upstream)
398
+ h.countFailure(di.Upstream)
399
}
400
}
401
402
// strike if the roundtrip took too long
403
if h.HealthChecks.Passive.UnhealthyLatency > 0 &&
404
latency >= time.Duration(h.HealthChecks.Passive.UnhealthyLatency) {
440
- h.countFailure(upstream)
405
+ h.countFailure(di.Upstream)
406
}
407
}
408
@@ -554,23 +519,21 @@ func (lb LoadBalancing) tryAgain(start time.Time, proxyErr error, req *http.Requ
519
return true
520
}
521
557
-// directRequest modifies only req.URL so that it points to the
558
-// given upstream host. It must modify ONLY the request URL.
559
-func (h Handler) directRequest(req *http.Request, upstream *Upstream) {
522
+// directRequest modifies only req.URL so that it points to the upstream
523
+// in the given DialInfo. It must modify ONLY the request URL.
524
+func (h Handler) directRequest(req *http.Request, di DialInfo) {
525
if req.URL.Host == "" {
526
// we need a host, so set the upstream's host address
562
- fullHost := upstream.dialInfo.Address
527
+ reqHost := di.Address
528
564
- // but if the port matches the scheme, strip the port because
529
+ // if the port equates to the scheme, strip the port because
530
// it's weird to make a request like http://example.com:80/.
566
- host, port, err := net.SplitHostPort(fullHost)
567
- if err == nil &&
568
- (req.URL.Scheme == "http" && port == "80") ||
569
- (req.URL.Scheme == "https" && port == "443") {
570
- fullHost = host
531
+ if (req.URL.Scheme == "http" && di.Port == "80") ||
532
+ (req.URL.Scheme == "https" && di.Port == "443") {
533
+ reqHost = di.Host
534
}
535
573
- req.URL.Host = fullHost
536
+ req.URL.Host = reqHost
537
}
538
}
539