@marcel91200 / caddy Mirror · read-only / commits / 1e31be8d
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: 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