@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 345 lines 9.4 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 "sync"
19 "testing"
20 "time"
21
22 "github.com/caddyserver/caddy/v2"
23 )
24
25 // resetDynamicHosts clears global dynamic host state between tests.
26 func resetDynamicHosts() {
27 dynamicHostsMu.Lock()
28 dynamicHosts = make(map[string]dynamicHostEntry)
29 dynamicHostsMu.Unlock()
30 // Reset the Once so cleanup goroutine tests can re-trigger if needed.
31 dynamicHostsCleanerOnce = sync.Once{}
32 }
33
34 // TestFillDynamicHostCreatesEntry verifies that calling fillDynamicHost on a
35 // new address inserts an entry into dynamicHosts and assigns a non-nil Host.
36 func TestFillDynamicHostCreatesEntry(t *testing.T) {
37 resetDynamicHosts()
38
39 u := &Upstream{Dial: "192.0.2.1:80"}
40 u.fillDynamicHost()
41
42 if u.Host == nil {
43 t.Fatal("expected Host to be set after fillDynamicHost")
44 }
45
46 dynamicHostsMu.RLock()
47 entry, ok := dynamicHosts["192.0.2.1:80"]
48 dynamicHostsMu.RUnlock()
49
50 if !ok {
51 t.Fatal("expected entry in dynamicHosts map")
52 }
53 if entry.host != u.Host {
54 t.Error("dynamicHosts entry host should be the same pointer assigned to Upstream.Host")
55 }
56 if entry.lastSeen.IsZero() {
57 t.Error("expected lastSeen to be set")
58 }
59 }
60
61 // TestFillDynamicHostReusesSameHost verifies that two calls for the same address
62 // return the exact same *Host pointer so that state (e.g. fail counts) is shared.
63 func TestFillDynamicHostReusesSameHost(t *testing.T) {
64 resetDynamicHosts()
65
66 u1 := &Upstream{Dial: "192.0.2.2:80"}
67 u1.fillDynamicHost()
68
69 u2 := &Upstream{Dial: "192.0.2.2:80"}
70 u2.fillDynamicHost()
71
72 if u1.Host != u2.Host {
73 t.Error("expected both upstreams to share the same *Host pointer")
74 }
75 }
76
77 // TestFillDynamicHostUpdatesLastSeen verifies that a second call for the same
78 // address advances the lastSeen timestamp.
79 func TestFillDynamicHostUpdatesLastSeen(t *testing.T) {
80 resetDynamicHosts()
81
82 u := &Upstream{Dial: "192.0.2.3:80"}
83 u.fillDynamicHost()
84
85 dynamicHostsMu.RLock()
86 first := dynamicHosts["192.0.2.3:80"].lastSeen
87 dynamicHostsMu.RUnlock()
88
89 // Ensure measurable time passes.
90 time.Sleep(2 * time.Millisecond)
91
92 u2 := &Upstream{Dial: "192.0.2.3:80"}
93 u2.fillDynamicHost()
94
95 dynamicHostsMu.RLock()
96 second := dynamicHosts["192.0.2.3:80"].lastSeen
97 dynamicHostsMu.RUnlock()
98
99 if !second.After(first) {
100 t.Error("expected lastSeen to be updated on second fillDynamicHost call")
101 }
102 }
103
104 // TestFillDynamicHostIndependentAddresses verifies that different addresses get
105 // independent Host entries.
106 func TestFillDynamicHostIndependentAddresses(t *testing.T) {
107 resetDynamicHosts()
108
109 u1 := &Upstream{Dial: "192.0.2.4:80"}
110 u1.fillDynamicHost()
111
112 u2 := &Upstream{Dial: "192.0.2.5:80"}
113 u2.fillDynamicHost()
114
115 if u1.Host == u2.Host {
116 t.Error("different addresses should have different *Host entries")
117 }
118 }
119
120 // TestFillDynamicHostPreservesFailCount verifies that fail counts on a dynamic
121 // host survive across multiple fillDynamicHost calls (simulating sequential
122 // requests), which is the core behaviour fixed by this change.
123 func TestFillDynamicHostPreservesFailCount(t *testing.T) {
124 resetDynamicHosts()
125
126 // First "request": provision and record a failure.
127 u1 := &Upstream{Dial: "192.0.2.6:80"}
128 u1.fillDynamicHost()
129 _ = u1.Host.countFail(1)
130
131 if u1.Host.Fails() != 1 {
132 t.Fatalf("expected 1 fail, got %d", u1.Host.Fails())
133 }
134
135 // Second "request": provision the same address again (new *Upstream, same address).
136 u2 := &Upstream{Dial: "192.0.2.6:80"}
137 u2.fillDynamicHost()
138
139 if u2.Host.Fails() != 1 {
140 t.Errorf("expected fail count to persist across fillDynamicHost calls, got %d", u2.Host.Fails())
141 }
142 }
143
144 // TestProvisionUpstreamDynamic verifies that provisionUpstream with dynamic=true
145 // uses fillDynamicHost (not the UsagePool) and sets healthCheckPolicy /
146 // MaxRequests correctly from handler config.
147 func TestProvisionUpstreamDynamic(t *testing.T) {
148 resetDynamicHosts()
149
150 passive := &PassiveHealthChecks{
151 FailDuration: caddy.Duration(10 * time.Second),
152 MaxFails: 3,
153 UnhealthyRequestCount: 5,
154 }
155 h := Handler{
156 HealthChecks: &HealthChecks{
157 Passive: passive,
158 },
159 }
160
161 u := &Upstream{Dial: "192.0.2.7:80"}
162 h.provisionUpstream(u, true)
163
164 if u.Host == nil {
165 t.Fatal("Host should be set after provisionUpstream")
166 }
167 if u.healthCheckPolicy != passive {
168 t.Error("healthCheckPolicy should point to the handler's PassiveHealthChecks")
169 }
170 if u.MaxRequests != 5 {
171 t.Errorf("expected MaxRequests=5 from UnhealthyRequestCount, got %d", u.MaxRequests)
172 }
173
174 // Must be in dynamicHosts, not in the static UsagePool.
175 dynamicHostsMu.RLock()
176 _, inDynamic := dynamicHosts["192.0.2.7:80"]
177 dynamicHostsMu.RUnlock()
178 if !inDynamic {
179 t.Error("dynamic upstream should be stored in dynamicHosts")
180 }
181 _, inPool := hosts.References("192.0.2.7:80")
182 if inPool {
183 t.Error("dynamic upstream should NOT be stored in the static UsagePool")
184 }
185 }
186
187 // TestProvisionUpstreamStatic verifies that provisionUpstream with dynamic=false
188 // uses the UsagePool and does NOT insert into dynamicHosts.
189 func TestProvisionUpstreamStatic(t *testing.T) {
190 resetDynamicHosts()
191
192 h := Handler{}
193
194 u := &Upstream{Dial: "192.0.2.8:80"}
195 h.provisionUpstream(u, false)
196
197 if u.Host == nil {
198 t.Fatal("Host should be set after provisionUpstream")
199 }
200
201 refs, inPool := hosts.References("192.0.2.8:80")
202 if !inPool {
203 t.Error("static upstream should be in the UsagePool")
204 }
205 if refs != 1 {
206 t.Errorf("expected ref count 1, got %d", refs)
207 }
208
209 dynamicHostsMu.RLock()
210 _, inDynamic := dynamicHosts["192.0.2.8:80"]
211 dynamicHostsMu.RUnlock()
212 if inDynamic {
213 t.Error("static upstream should NOT be in dynamicHosts")
214 }
215
216 // Clean up the pool entry we just added.
217 _, _ = hosts.Delete("192.0.2.8:80")
218 }
219
220 // TestDynamicHostHealthyConsultsFails verifies the end-to-end passive health
221 // check path: after enough failures are recorded against a dynamic upstream's
222 // shared *Host, Healthy() returns false for a newly provisioned *Upstream with
223 // the same address.
224 func TestDynamicHostHealthyConsultsFails(t *testing.T) {
225 resetDynamicHosts()
226
227 passive := &PassiveHealthChecks{
228 FailDuration: caddy.Duration(time.Minute),
229 MaxFails: 2,
230 }
231 h := Handler{
232 HealthChecks: &HealthChecks{Passive: passive},
233 }
234
235 // First request: provision and record two failures.
236 u1 := &Upstream{Dial: "192.0.2.9:80"}
237 h.provisionUpstream(u1, true)
238
239 _ = u1.Host.countFail(1)
240 _ = u1.Host.countFail(1)
241
242 // Second request: fresh *Upstream, same address.
243 u2 := &Upstream{Dial: "192.0.2.9:80"}
244 h.provisionUpstream(u2, true)
245
246 if u2.Healthy() {
247 t.Error("upstream should be unhealthy after MaxFails failures have been recorded against its shared Host")
248 }
249 }
250
251 // TestDynamicHostCleanupEvictsStaleEntries verifies that the cleanup sweep
252 // removes entries whose lastSeen is older than dynamicHostIdleExpiry.
253 func TestDynamicHostCleanupEvictsStaleEntries(t *testing.T) {
254 resetDynamicHosts()
255
256 const addr = "192.0.2.10:80"
257
258 // Insert an entry directly with a lastSeen far in the past.
259 dynamicHostsMu.Lock()
260 dynamicHosts[addr] = dynamicHostEntry{
261 host: new(Host),
262 lastSeen: time.Now().Add(-2 * dynamicHostIdleExpiry),
263 }
264 dynamicHostsMu.Unlock()
265
266 // Run the cleanup logic inline (same logic as the goroutine).
267 dynamicHostsMu.Lock()
268 for a, entry := range dynamicHosts {
269 if time.Since(entry.lastSeen) > dynamicHostIdleExpiry {
270 delete(dynamicHosts, a)
271 }
272 }
273 dynamicHostsMu.Unlock()
274
275 dynamicHostsMu.RLock()
276 _, stillPresent := dynamicHosts[addr]
277 dynamicHostsMu.RUnlock()
278
279 if stillPresent {
280 t.Error("stale dynamic host entry should have been evicted by cleanup sweep")
281 }
282 }
283
284 // TestDynamicHostCleanupRetainsFreshEntries verifies that the cleanup sweep
285 // keeps entries whose lastSeen is within dynamicHostIdleExpiry.
286 func TestDynamicHostCleanupRetainsFreshEntries(t *testing.T) {
287 resetDynamicHosts()
288
289 const addr = "192.0.2.11:80"
290
291 dynamicHostsMu.Lock()
292 dynamicHosts[addr] = dynamicHostEntry{
293 host: new(Host),
294 lastSeen: time.Now(),
295 }
296 dynamicHostsMu.Unlock()
297
298 // Run the cleanup logic inline.
299 dynamicHostsMu.Lock()
300 for a, entry := range dynamicHosts {
301 if time.Since(entry.lastSeen) > dynamicHostIdleExpiry {
302 delete(dynamicHosts, a)
303 }
304 }
305 dynamicHostsMu.Unlock()
306
307 dynamicHostsMu.RLock()
308 _, stillPresent := dynamicHosts[addr]
309 dynamicHostsMu.RUnlock()
310
311 if !stillPresent {
312 t.Error("fresh dynamic host entry should be retained by cleanup sweep")
313 }
314 }
315
316 // TestDynamicHostConcurrentFillHost verifies that concurrent calls to
317 // fillDynamicHost for the same address all get the same *Host pointer and
318 // don't race (run with -race).
319 func TestDynamicHostConcurrentFillHost(t *testing.T) {
320 resetDynamicHosts()
321
322 const addr = "192.0.2.12:80"
323 const goroutines = 50
324
325 var wg sync.WaitGroup
326 hosts := make([]*Host, goroutines)
327
328 for i := range goroutines {
329 wg.Add(1)
330 go func(idx int) {
331 defer wg.Done()
332 u := &Upstream{Dial: addr}
333 u.fillDynamicHost()
334 hosts[idx] = u.Host
335 }(i)
336 }
337 wg.Wait()
338
339 first := hosts[0]
340 for i, h := range hosts {
341 if h != first {
342 t.Errorf("goroutine %d got a different *Host pointer; expected all to share the same entry", i)
343 }
344 }
345 }