@marcel91200 / caddy Mirror · read-only
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 11, 01:00 UTC

master
go 640 lines 20.3 KB
Raw
1 // Copyright 2015 Matthew Holt and The Caddy Authors
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14
15 package reverseproxy
16
17 import (
18 "context"
19 "fmt"
20 "io"
21 "net"
22 "net/http"
23 "net/url"
24 "regexp"
25 "runtime/debug"
26 "strconv"
27 "strings"
28 "time"
29
30 "go.uber.org/zap"
31 "go.uber.org/zap/zapcore"
32
33 "github.com/caddyserver/caddy/v2"
34 "github.com/caddyserver/caddy/v2/modules/caddyhttp"
35 )
36
37 // HealthChecks configures active and passive health checks.
38 type HealthChecks struct {
39 // Active health checks run in the background on a timer. To
40 // minimally enable active health checks, set either path or
41 // port (or both). Note that active health check status
42 // (healthy/unhealthy) is stored per-proxy-handler, not
43 // globally; this allows different handlers to use different
44 // criteria to decide what defines a healthy backend.
45 //
46 // Active health checks do not run for dynamic upstreams.
47 Active *ActiveHealthChecks `json:"active,omitempty"`
48
49 // Passive health checks monitor proxied requests for errors or timeouts.
50 // To minimally enable passive health checks, specify at least an empty
51 // config object with fail_duration > 0. Passive health check state is
52 // shared (stored globally), so a failure from one handler will be counted
53 // by all handlers; but the tolerances or standards for what defines
54 // healthy/unhealthy backends is configured per-proxy-handler.
55 //
56 // Passive health checks technically do operate on dynamic upstreams,
57 // but are only effective for very busy proxies where the list of
58 // upstreams is mostly stable. This is because the shared/global
59 // state of upstreams is cleaned up when the upstreams are no longer
60 // used. Since dynamic upstreams are allocated dynamically at each
61 // request (specifically, each iteration of the proxy loop per request),
62 // they are also cleaned up after every request. Thus, if there is a
63 // moment when no requests are actively referring to a particular
64 // upstream host, the passive health check state will be reset because
65 // it will be garbage-collected. It is usually better for the dynamic
66 // upstream module to only return healthy, available backends instead.
67 Passive *PassiveHealthChecks `json:"passive,omitempty"`
68 }
69
70 // ActiveHealthChecks holds configuration related to active
71 // health checks (that is, health checks which occur in a
72 // background goroutine independently).
73 type ActiveHealthChecks struct {
74 // Deprecated: Use 'uri' instead. This field will be removed. TODO: remove this field
75 Path string `json:"path,omitempty"`
76
77 // The URI (path and query) to use for health checks
78 URI string `json:"uri,omitempty"`
79
80 // The host:port to use (if different from the upstream's dial address)
81 // for health checks. This should be used in tandem with `health_header` and
82 // `{http.reverse_proxy.active.target_upstream}`. This can be helpful when
83 // creating an intermediate service to do a more thorough health check.
84 // If upstream is set, the active health check port is ignored.
85 Upstream string `json:"upstream,omitempty"`
86
87 // The port to use (if different from the upstream's dial
88 // address) for health checks. If active upstream is set,
89 // this value is ignored.
90 Port int `json:"port,omitempty"`
91
92 // HTTP headers to set on health check requests.
93 Headers http.Header `json:"headers,omitempty"`
94
95 // The HTTP method to use for health checks (default "GET").
96 Method string `json:"method,omitempty"`
97
98 // The body to send with the health check request.
99 Body string `json:"body,omitempty"`
100
101 // Whether to follow HTTP redirects in response to active health checks (default off).
102 FollowRedirects bool `json:"follow_redirects,omitempty"`
103
104 // How frequently to perform active health checks (default 30s).
105 Interval caddy.Duration `json:"interval,omitempty"`
106
107 // How long to wait for a response from a backend before
108 // considering it unhealthy (default 5s).
109 Timeout caddy.Duration `json:"timeout,omitempty"`
110
111 // Number of consecutive health check passes before marking
112 // a previously unhealthy backend as healthy again (default 1).
113 Passes int `json:"passes,omitempty"`
114
115 // Number of consecutive health check failures before marking
116 // a previously healthy backend as unhealthy (default 1).
117 Fails int `json:"fails,omitempty"`
118
119 // The maximum response body to download from the backend
120 // during a health check.
121 MaxSize int64 `json:"max_size,omitempty"`
122
123 // The HTTP status code to expect from a healthy backend.
124 ExpectStatus int `json:"expect_status,omitempty"`
125
126 // A regular expression against which to match the response
127 // body of a healthy backend.
128 ExpectBody string `json:"expect_body,omitempty"`
129
130 uri *url.URL
131 httpClient *http.Client
132 bodyRegexp *regexp.Regexp
133 logger *zap.Logger
134 }
135
136 // Provision ensures that a is set up properly before use.
137 func (a *ActiveHealthChecks) Provision(ctx caddy.Context, h *Handler) error {
138 if !a.IsEnabled() {
139 return nil
140 }
141
142 // Canonicalize the header keys ahead of time, since
143 // JSON unmarshaled headers may be incorrect
144 cleaned := http.Header{}
145 for key, hdrs := range a.Headers {
146 for _, val := range hdrs {
147 cleaned.Add(key, val)
148 }
149 }
150 a.Headers = cleaned
151
152 // If Method is not set, default to GET
153 if a.Method == "" {
154 a.Method = http.MethodGet
155 }
156
157 h.HealthChecks.Active.logger = h.logger.Named("health_checker.active")
158
159 timeout := time.Duration(a.Timeout)
160 if timeout == 0 {
161 timeout = 5 * time.Second
162 }
163
164 if a.Path != "" {
165 a.logger.Warn("the 'path' option is deprecated, please use 'uri' instead!")
166 }
167
168 // parse the URI string (supports path and query)
169 if a.URI != "" {
170 parsedURI, err := url.Parse(a.URI)
171 if err != nil {
172 return err
173 }
174 a.uri = parsedURI
175 }
176
177 a.httpClient = &http.Client{
178 Timeout: timeout,
179 Transport: h.Transport,
180 CheckRedirect: func(req *http.Request, via []*http.Request) error {
181 if !a.FollowRedirects {
182 return http.ErrUseLastResponse
183 }
184 return nil
185 },
186 }
187
188 for _, upstream := range h.Upstreams {
189 // if there's an alternative upstream for health-check provided in the config,
190 // then use it, otherwise use the upstream's dial address. if upstream is used,
191 // then the port is ignored.
192 if a.Upstream != "" {
193 upstream.activeHealthCheckUpstream = a.Upstream
194 } else if a.Port != 0 {
195 // if there's an alternative port for health-check provided in the config,
196 // then use it, otherwise use the port of upstream.
197 upstream.activeHealthCheckPort = a.Port
198 }
199 }
200
201 if a.Interval == 0 {
202 a.Interval = caddy.Duration(30 * time.Second)
203 }
204
205 if a.ExpectBody != "" {
206 var err error
207 a.bodyRegexp, err = regexp.Compile(a.ExpectBody)
208 if err != nil {
209 return fmt.Errorf("expect_body: compiling regular expression: %v", err)
210 }
211 }
212
213 if a.Passes < 1 {
214 a.Passes = 1
215 }
216
217 if a.Fails < 1 {
218 a.Fails = 1
219 }
220
221 return nil
222 }
223
224 // IsEnabled checks if the active health checks have
225 // the minimum config necessary to be enabled.
226 func (a *ActiveHealthChecks) IsEnabled() bool {
227 return a.Path != "" || a.URI != "" || a.Port != 0
228 }
229
230 // PassiveHealthChecks holds configuration related to passive
231 // health checks (that is, health checks which occur during
232 // the normal flow of request proxying).
233 type PassiveHealthChecks struct {
234 // How long to remember a failed request to a backend. A duration > 0
235 // enables passive health checking. Default is 0.
236 FailDuration caddy.Duration `json:"fail_duration,omitempty"`
237
238 // The number of failed requests within the FailDuration window to
239 // consider a backend as "down". Must be >= 1; default is 1. Requires
240 // that FailDuration be > 0.
241 MaxFails int `json:"max_fails,omitempty"`
242
243 // Limits the number of simultaneous requests to a backend by
244 // marking the backend as "down" if it has this many concurrent
245 // requests or more.
246 UnhealthyRequestCount int `json:"unhealthy_request_count,omitempty"`
247
248 // Count the request as failed if the response comes back with
249 // one of these status codes.
250 UnhealthyStatus []int `json:"unhealthy_status,omitempty"`
251
252 // Count the request as failed if the response takes at least this
253 // long to receive.
254 UnhealthyLatency caddy.Duration `json:"unhealthy_latency,omitempty"`
255
256 logger *zap.Logger
257 }
258
259 // CircuitBreaker is a type that can act as an early-warning
260 // system for the health checker when backends are getting
261 // overloaded. This interface is still experimental and is
262 // subject to change.
263 type CircuitBreaker interface {
264 OK() bool
265 RecordMetric(statusCode int, latency time.Duration)
266 }
267
268 // activeHealthChecker runs active health checks on a
269 // regular basis and blocks until
270 // h.HealthChecks.Active.stopChan is closed.
271 func (h *Handler) activeHealthChecker() {
272 defer func() {
273 if err := recover(); err != nil {
274 if c := h.HealthChecks.Active.logger.Check(zapcore.ErrorLevel, "active health checker panicked"); c != nil {
275 c.Write(
276 zap.Any("error", err),
277 zap.ByteString("stack", debug.Stack()),
278 )
279 }
280 }
281 }()
282 ticker := time.NewTicker(time.Duration(h.HealthChecks.Active.Interval))
283 h.doActiveHealthCheckForAllHosts()
284 for {
285 select {
286 case <-ticker.C:
287 h.doActiveHealthCheckForAllHosts()
288 case <-h.ctx.Done():
289 ticker.Stop()
290 return
291 }
292 }
293 }
294
295 // doActiveHealthCheckForAllHosts immediately performs a
296 // health checks for all upstream hosts configured by h.
297 func (h *Handler) doActiveHealthCheckForAllHosts() {
298 for _, upstream := range h.Upstreams {
299 go func(upstream *Upstream) {
300 defer func() {
301 if err := recover(); err != nil {
302 if c := h.HealthChecks.Active.logger.Check(zapcore.ErrorLevel, "active health checker panicked"); c != nil {
303 c.Write(
304 zap.Any("error", err),
305 zap.ByteString("stack", debug.Stack()),
306 )
307 }
308 }
309 }()
310
311 repl := caddy.NewReplacer()
312
313 networkAddr, err := repl.ReplaceOrErr(upstream.Dial, true, true)
314 if err != nil {
315 if c := h.HealthChecks.Active.logger.Check(zapcore.ErrorLevel, "invalid use of placeholders in dial address for active health checks"); c != nil {
316 c.Write(
317 zap.String("address", networkAddr),
318 zap.Error(err),
319 )
320 }
321 return
322 }
323 addr, err := caddy.ParseNetworkAddress(networkAddr)
324 if err != nil {
325 if c := h.HealthChecks.Active.logger.Check(zapcore.ErrorLevel, "bad network address"); c != nil {
326 c.Write(
327 zap.String("address", networkAddr),
328 zap.Error(err),
329 )
330 }
331 return
332 }
333 if hcp := uint(upstream.activeHealthCheckPort); hcp != 0 {
334 if addr.IsUnixNetwork() || addr.IsFdNetwork() {
335 addr.Network = "tcp" // I guess we just assume TCP since we are using a port??
336 }
337 addr.StartPort, addr.EndPort = hcp, hcp
338 }
339 if addr.PortRangeSize() != 1 {
340 if c := h.HealthChecks.Active.logger.Check(zapcore.ErrorLevel, "multiple addresses (upstream must map to only one address)"); c != nil {
341 c.Write(
342 zap.String("address", networkAddr),
343 )
344 }
345 return
346 }
347 hostAddr := addr.JoinHostPort(0)
348 if addr.IsUnixNetwork() || addr.IsFdNetwork() {
349 // this will be used as the Host portion of a http.Request URL, and
350 // paths to socket files would produce an error when creating URL,
351 // so use a fake Host value instead; unix sockets are usually local
352 hostAddr = "localhost"
353 }
354
355 // Fill in the dial info for the upstream
356 // If the upstream is set, use that instead
357 dialInfoUpstream := upstream
358 if h.HealthChecks.Active.Upstream != "" {
359 dialInfoUpstream = &Upstream{
360 Dial: h.HealthChecks.Active.Upstream,
361 }
362 } else if upstream.activeHealthCheckPort != 0 {
363 // health_port overrides the port; addr has already been updated
364 // with the health port, so use its address for dialing
365 dialInfoUpstream = &Upstream{
366 Dial: addr.JoinHostPort(0),
367 }
368 }
369 dialInfo, _ := dialInfoUpstream.fillDialInfo(repl)
370
371 err = h.doActiveHealthCheck(dialInfo, hostAddr, networkAddr, upstream)
372 if err != nil {
373 if c := h.HealthChecks.Active.logger.Check(zapcore.ErrorLevel, "active health check failed"); c != nil {
374 c.Write(
375 zap.String("address", hostAddr),
376 zap.Error(err),
377 )
378 }
379 }
380 }(upstream)
381 }
382 }
383
384 // doActiveHealthCheck performs a health check to upstream which
385 // can be reached at address hostAddr. The actual address for
386 // the request will be built according to active health checker
387 // config. The health status of the host will be updated
388 // according to whether it passes the health check. An error is
389 // returned only if the health check fails to occur or if marking
390 // the host's health status fails.
391 func (h *Handler) doActiveHealthCheck(dialInfo DialInfo, hostAddr string, networkAddr string, upstream *Upstream) error {
392 // create the URL for the request that acts as a health check
393 u := &url.URL{
394 Scheme: "http",
395 Host: hostAddr,
396 }
397
398 // split the host and port if possible, override the port if configured
399 host, port, err := net.SplitHostPort(hostAddr)
400 if err != nil {
401 host = hostAddr
402 }
403
404 // ignore active health check port if active upstream is provided as the
405 // active upstream already contains the replacement port
406 if h.HealthChecks.Active.Upstream != "" {
407 u.Host = h.HealthChecks.Active.Upstream
408 } else if h.HealthChecks.Active.Port != 0 {
409 port := strconv.Itoa(h.HealthChecks.Active.Port)
410 u.Host = net.JoinHostPort(host, port)
411 }
412
413 // override health check schemes if applicable
414 if hcsot, ok := h.Transport.(HealthCheckSchemeOverriderTransport); ok {
415 hcsot.OverrideHealthCheckScheme(u, port)
416 }
417
418 // if we have a provisioned uri, use that, otherwise use
419 // the deprecated Path option
420 if h.HealthChecks.Active.uri != nil {
421 u.Path = h.HealthChecks.Active.uri.Path
422 u.RawQuery = h.HealthChecks.Active.uri.RawQuery
423 } else {
424 u.Path = h.HealthChecks.Active.Path
425 }
426
427 // replacer used for both body and headers. Only globals (env vars, system info, etc.) are available
428 repl := caddy.NewReplacer()
429
430 // if body is provided, create a reader for it, otherwise nil
431 var requestBody io.Reader
432 if h.HealthChecks.Active.Body != "" {
433 // Set body, using replacer. Only known placeholders are replaced: the
434 // body is often JSON, whose braces would otherwise be read as
435 // placeholders and blanked, sending an empty body instead.
436 requestBody = strings.NewReader(repl.ReplaceKnown(h.HealthChecks.Active.Body, ""))
437 }
438
439 // attach dialing information to this request, as well as context values that
440 // may be expected by handlers of this request
441 ctx := h.ctx.Context
442 ctx = context.WithValue(ctx, caddy.ReplacerCtxKey, caddy.NewReplacer())
443 ctx = context.WithValue(ctx, dialInfoCtxKey, dialInfo)
444 req, err := http.NewRequestWithContext(ctx, h.HealthChecks.Active.Method, u.String(), requestBody)
445 if err != nil {
446 return fmt.Errorf("making request: %v", err)
447 }
448 ctx = context.WithValue(ctx, caddyhttp.OriginalRequestCtxKey, *req)
449 req = req.WithContext(ctx)
450
451 // set headers, using replacer
452 repl.Set("http.reverse_proxy.active.target_upstream", networkAddr)
453 for key, vals := range h.HealthChecks.Active.Headers {
454 key = repl.ReplaceAll(key, "")
455 if key == "Host" {
456 req.Host = repl.ReplaceAll(h.HealthChecks.Active.Headers.Get(key), "")
457 continue
458 }
459 for _, val := range vals {
460 req.Header.Add(key, repl.ReplaceKnown(val, ""))
461 }
462 }
463
464 markUnhealthy := func() {
465 // increment failures and then check if it has reached the threshold to mark unhealthy
466 err := upstream.countHealthFail(1)
467 if err != nil {
468 if c := h.HealthChecks.Active.logger.Check(zapcore.ErrorLevel, "could not count active health failure"); c != nil {
469 c.Write(
470 zap.String("host", upstream.Dial),
471 zap.Error(err),
472 )
473 }
474 return
475 }
476 if upstream.activeHealthFails() >= h.HealthChecks.Active.Fails {
477 // dispatch an event that the host newly became unhealthy
478 if upstream.setHealthy(false) {
479 h.events.Emit(h.ctx, "unhealthy", map[string]any{"host": hostAddr})
480 upstream.resetHealth()
481 }
482 }
483 }
484
485 markHealthy := func() {
486 // increment passes and then check if it has reached the threshold to be healthy
487 err := upstream.countHealthPass(1)
488 if err != nil {
489 if c := h.HealthChecks.Active.logger.Check(zapcore.ErrorLevel, "could not count active health pass"); c != nil {
490 c.Write(
491 zap.String("host", upstream.Dial),
492 zap.Error(err),
493 )
494 }
495 return
496 }
497 if upstream.activeHealthPasses() >= h.HealthChecks.Active.Passes {
498 if upstream.setHealthy(true) {
499 if c := h.HealthChecks.Active.logger.Check(zapcore.InfoLevel, "host is up"); c != nil {
500 c.Write(zap.String("host", hostAddr))
501 }
502 h.events.Emit(h.ctx, "healthy", map[string]any{"host": hostAddr})
503 upstream.resetHealth()
504 }
505 }
506 }
507
508 // do the request, being careful to tame the response body
509 resp, err := h.HealthChecks.Active.httpClient.Do(req) //nolint:gosec // no SSRF
510 if err != nil {
511 if c := h.HealthChecks.Active.logger.Check(zapcore.InfoLevel, "HTTP request failed"); c != nil {
512 c.Write(
513 zap.String("host", hostAddr),
514 zap.Error(err),
515 )
516 }
517 markUnhealthy()
518 return nil
519 }
520 var body io.Reader = resp.Body
521 if h.HealthChecks.Active.MaxSize > 0 {
522 body = io.LimitReader(body, h.HealthChecks.Active.MaxSize)
523 }
524 defer func() {
525 // drain any remaining body so connection could be reused
526 _, _ = io.Copy(io.Discard, body)
527 resp.Body.Close()
528 }()
529
530 // if status code is outside criteria, mark down
531 if h.HealthChecks.Active.ExpectStatus > 0 {
532 if !caddyhttp.StatusCodeMatches(resp.StatusCode, h.HealthChecks.Active.ExpectStatus) {
533 if c := h.HealthChecks.Active.logger.Check(zapcore.InfoLevel, "unexpected status code"); c != nil {
534 c.Write(
535 zap.Int("status_code", resp.StatusCode),
536 zap.String("host", hostAddr),
537 )
538 }
539 markUnhealthy()
540 return nil
541 }
542 } else if resp.StatusCode < 200 || resp.StatusCode >= 300 {
543 if c := h.HealthChecks.Active.logger.Check(zapcore.InfoLevel, "status code out of tolerances"); c != nil {
544 c.Write(
545 zap.Int("status_code", resp.StatusCode),
546 zap.String("host", hostAddr),
547 )
548 }
549 markUnhealthy()
550 return nil
551 }
552
553 // if body does not match regex, mark down
554 if h.HealthChecks.Active.bodyRegexp != nil {
555 bodyBytes, err := io.ReadAll(body)
556 if err != nil {
557 if c := h.HealthChecks.Active.logger.Check(zapcore.InfoLevel, "failed to read response body"); c != nil {
558 c.Write(
559 zap.String("host", hostAddr),
560 zap.Error(err),
561 )
562 }
563 markUnhealthy()
564 return nil
565 }
566 if !h.HealthChecks.Active.bodyRegexp.Match(bodyBytes) {
567 if c := h.HealthChecks.Active.logger.Check(zapcore.InfoLevel, "response body failed expectations"); c != nil {
568 c.Write(
569 zap.String("host", hostAddr),
570 )
571 }
572 markUnhealthy()
573 return nil
574 }
575 }
576
577 // passed health check parameters, so mark as healthy
578 markHealthy()
579
580 return nil
581 }
582
583 // countFailure is used with passive health checks. It
584 // remembers 1 failure for upstream for the configured
585 // duration. If passive health checks are disabled or
586 // failure expiry is 0, this is a no-op.
587 func (h *Handler) countFailure(upstream *Upstream) {
588 // only count failures if passive health checking is enabled
589 // and if failures are configured have a non-zero expiry
590 if h.HealthChecks == nil || h.HealthChecks.Passive == nil {
591 return
592 }
593 failDuration := time.Duration(h.HealthChecks.Passive.FailDuration)
594 if failDuration == 0 {
595 return
596 }
597
598 // count failure immediately
599 err := upstream.Host.countFail(1)
600 if err != nil {
601 if c := h.HealthChecks.Active.logger.Check(zapcore.ErrorLevel, "could not count failure"); c != nil {
602 c.Write(
603 zap.String("host", upstream.Dial),
604 zap.Error(err),
605 )
606 }
607 return
608 }
609
610 // forget it later
611 go func(host *Host, failDuration time.Duration) {
612 defer func() {
613 if err := recover(); err != nil {
614 if c := h.HealthChecks.Active.logger.Check(zapcore.ErrorLevel, "passive health check failure forgetter panicked"); c != nil {
615 c.Write(
616 zap.Any("error", err),
617 zap.ByteString("stack", debug.Stack()),
618 )
619 }
620 }
621 }()
622 timer := time.NewTimer(failDuration)
623 select {
624 case <-h.ctx.Done():
625 if !timer.Stop() {
626 <-timer.C
627 }
628 case <-timer.C:
629 }
630 err := host.countFail(-1)
631 if err != nil {
632 if c := h.HealthChecks.Active.logger.Check(zapcore.ErrorLevel, "could not forget failure"); c != nil {
633 c.Write(
634 zap.String("host", upstream.Dial),
635 zap.Error(err),
636 )
637 }
638 }
639 }(upstream.Host, failDuration)
640 }