@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 220 lines 7.11 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 "io"
20 "net/http"
21 "net/http/httptest"
22 "strings"
23 "sync/atomic"
24 "testing"
25
26 "go.uber.org/zap"
27
28 "github.com/caddyserver/caddy/v2"
29 "github.com/caddyserver/caddy/v2/modules/caddyevents"
30 )
31
32 // newActiveHandler builds a minimal Handler with active health checks
33 // configured against addr, provisions its single upstream, and returns
34 // the handler, its upstream, and a cancel func the caller must defer.
35 func newActiveHandler(t *testing.T, addr, uri string, fails int) (*Handler, *Upstream, context.CancelFunc) {
36 t.Helper()
37 caddyCtx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
38
39 eventsApp := new(caddyevents.App)
40 if err := eventsApp.Provision(caddyCtx); err != nil {
41 t.Fatalf("provisioning events app: %v", err)
42 }
43
44 u := &Upstream{Dial: addr}
45 h := &Handler{
46 ctx: caddyCtx,
47 logger: zap.NewNop(),
48 events: eventsApp,
49 Upstreams: UpstreamPool{u},
50 HealthChecks: &HealthChecks{
51 Active: &ActiveHealthChecks{
52 URI: uri,
53 Fails: fails,
54 },
55 },
56 }
57 h.provisionUpstream(u, false)
58 if err := h.HealthChecks.Active.Provision(caddyCtx, h); err != nil {
59 t.Fatalf("provisioning active health checks: %v", err)
60 }
61 return h, u, cancel
62 }
63
64 // runActiveHealthCheck synchronously performs one active health check round
65 // for the handler's upstream (what doActiveHealthCheckForAllHosts does per
66 // tick, minus the goroutine).
67 func runActiveHealthCheck(t *testing.T, h *Handler, u *Upstream) {
68 t.Helper()
69 dialInfo, err := u.fillDialInfo(caddy.NewReplacer())
70 if err != nil {
71 t.Fatalf("filling dial info: %v", err)
72 }
73 if err := h.doActiveHealthCheck(dialInfo, dialInfo.Address, u.Dial, u); err != nil {
74 t.Fatalf("active health check: %v", err)
75 }
76 }
77
78 // drainHostsPool removes every entry from the global static host pool,
79 // deleting each exactly as many times as it was stored so the pool is
80 // empty for subsequent tests.
81 func drainHostsPool() {
82 var keys []any
83 hosts.Range(func(key, _ any) bool {
84 keys = append(keys, key)
85 return true
86 })
87 for _, key := range keys {
88 if refs, ok := hosts.References(key); ok {
89 for range refs {
90 _, _ = hosts.Delete(key)
91 }
92 }
93 }
94 }
95
96 // TestActiveHealthChecksSameAddressDifferentChecksAreIndependent is a
97 // regression test for https://github.com/caddyserver/caddy/issues/7870:
98 // two handlers that dial the same upstream address but run different
99 // active health checks (different health_uri) must keep independent
100 // health state. Before the fix, the consecutive pass/fail counters lived
101 // on the Host, which is shared by dial address, so both checkers mutated
102 // the same counters: one vhost's failing probes could push the other
103 // vhost's upstream over its own fails threshold and knock it out of that
104 // vhost's data path.
105 func TestActiveHealthChecksSameAddressDifferentChecksAreIndependent(t *testing.T) {
106 resetDynamicHosts()
107 defer drainHostsPool()
108
109 // one backend node serving two vhosts: vhost A's health endpoint is
110 // down, vhost B's is up (except for one transient failure below)
111 var vhostBUp atomic.Bool
112 vhostBUp.Store(true)
113 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
114 switch r.URL.Path {
115 case "/vhost-a/health":
116 w.WriteHeader(http.StatusServiceUnavailable)
117 case "/vhost-b/health":
118 if vhostBUp.Load() {
119 w.WriteHeader(http.StatusOK)
120 } else {
121 w.WriteHeader(http.StatusServiceUnavailable)
122 }
123 default:
124 w.WriteHeader(http.StatusNotFound)
125 }
126 }))
127 defer srv.Close()
128 addr := strings.TrimPrefix(srv.URL, "http://")
129
130 hA, uA, cancelA := newActiveHandler(t, addr, "/vhost-a/health", 5)
131 defer cancelA()
132 hB, uB, cancelB := newActiveHandler(t, addr, "/vhost-b/health", 3)
133 defer cancelB()
134
135 // vhost A's checker observes two consecutive failures — below its own
136 // fails=5 threshold, so these must affect no one's health status
137 runActiveHealthCheck(t, hA, uA)
138 runActiveHealthCheck(t, hA, uA)
139
140 // vhost B's checker observes a single transient failure; B tolerates
141 // up to fails=3 consecutive failures, so it must remain healthy
142 vhostBUp.Store(false)
143 runActiveHealthCheck(t, hB, uB)
144
145 if !uA.Healthy() {
146 t.Errorf("vhost A's upstream should still be healthy after 2 of 5 tolerated failures")
147 }
148 if !uB.Healthy() {
149 t.Errorf("vhost B's upstream was marked unhealthy after a single failed probe (fails=3); " +
150 "its health state was polluted by vhost A's health check against the same address")
151 }
152 }
153
154 // TestActiveHealthCheckBodyKeepsUnknownPlaceholders is a regression test for
155 // https://github.com/caddyserver/caddy/issues/7021: an active health check
156 // body is user-supplied and is commonly JSON, so its braces must not be read
157 // as placeholders and blanked. Only globals are available to this replacer, so
158 // replacing everything sent an empty body for any JSON health_request_body.
159 func TestActiveHealthCheckBodyKeepsUnknownPlaceholders(t *testing.T) {
160 resetDynamicHosts()
161 defer drainHostsPool()
162
163 const jsonBody = `{"jsonrpc":"2.0","id":1,"method":"eth_blockNumber","params":[]}`
164
165 var gotBody atomic.Value
166 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
167 body, err := io.ReadAll(r.Body)
168 if err != nil {
169 t.Errorf("reading request body: %v", err)
170 }
171 gotBody.Store(string(body))
172 w.WriteHeader(http.StatusOK)
173 }))
174 defer srv.Close()
175 addr := strings.TrimPrefix(srv.URL, "http://")
176
177 h, u, cancel := newActiveHandler(t, addr, "/health", 1)
178 defer cancel()
179
180 h.HealthChecks.Active.Body = jsonBody
181
182 runActiveHealthCheck(t, h, u)
183
184 if got, want := gotBody.Load(), jsonBody; got != want {
185 t.Errorf("health check body = %q, want %q", got, want)
186 }
187 }
188
189 // TestActiveHealthCheckBodyStillReplacesKnownPlaceholders guards the other
190 // direction: switching to ReplaceKnown must not stop real placeholders from
191 // being replaced.
192 func TestActiveHealthCheckBodyStillReplacesKnownPlaceholders(t *testing.T) {
193 resetDynamicHosts()
194 defer drainHostsPool()
195
196 t.Setenv("CADDY_TEST_HEALTH_TOKEN", "s3cret")
197
198 var gotBody atomic.Value
199 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
200 body, err := io.ReadAll(r.Body)
201 if err != nil {
202 t.Errorf("reading request body: %v", err)
203 }
204 gotBody.Store(string(body))
205 w.WriteHeader(http.StatusOK)
206 }))
207 defer srv.Close()
208 addr := strings.TrimPrefix(srv.URL, "http://")
209
210 h, u, cancel := newActiveHandler(t, addr, "/health", 1)
211 defer cancel()
212
213 h.HealthChecks.Active.Body = `{"token":"{env.CADDY_TEST_HEALTH_TOKEN}"}`
214
215 runActiveHealthCheck(t, h, u)
216
217 if got, want := gotBody.Load(), `{"token":"s3cret"}`; got != want {
218 t.Errorf("health check body = %q, want %q", got, want)
219 }
220 }