@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 1,023 lines 29.7 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 "net/http"
20 "net/http/httptest"
21 "testing"
22
23 "github.com/caddyserver/caddy/v2"
24 "github.com/caddyserver/caddy/v2/caddyconfig"
25 "github.com/caddyserver/caddy/v2/modules/caddyhttp"
26 )
27
28 func testPool() UpstreamPool {
29 return UpstreamPool{
30 {Host: new(Host), Dial: "0.0.0.1"},
31 {Host: new(Host), Dial: "0.0.0.2"},
32 {Host: new(Host), Dial: "0.0.0.3"},
33 }
34 }
35
36 func TestRoundRobinPolicy(t *testing.T) {
37 pool := testPool()
38 rrPolicy := RoundRobinSelection{}
39 req, _ := http.NewRequest("GET", "/", nil)
40
41 h := rrPolicy.Select(pool, req, nil)
42 // First selected host is 1, because counter starts at 0
43 // and increments before host is selected
44 if h != pool[1] {
45 t.Error("Expected first round robin host to be second host in the pool.")
46 }
47 h = rrPolicy.Select(pool, req, nil)
48 if h != pool[2] {
49 t.Error("Expected second round robin host to be third host in the pool.")
50 }
51 h = rrPolicy.Select(pool, req, nil)
52 if h != pool[0] {
53 t.Error("Expected third round robin host to be first host in the pool.")
54 }
55 // mark host as down
56 pool[1].setHealthy(false)
57 h = rrPolicy.Select(pool, req, nil)
58 if h != pool[2] {
59 t.Error("Expected to skip down host.")
60 }
61 // mark host as up
62 pool[1].setHealthy(true)
63
64 h = rrPolicy.Select(pool, req, nil)
65 if h == pool[2] {
66 t.Error("Expected to balance evenly among healthy hosts")
67 }
68 // mark host as full
69 pool[1].countRequest(1)
70 pool[1].MaxRequests = 1
71 h = rrPolicy.Select(pool, req, nil)
72 if h != pool[2] {
73 t.Error("Expected to skip full host.")
74 }
75 }
76
77 func TestWeightedRoundRobinPolicy(t *testing.T) {
78 pool := testPool()
79 wrrPolicy := WeightedRoundRobinSelection{
80 Weights: []int{3, 2, 1},
81 totalWeight: 6,
82 }
83 req, _ := http.NewRequest("GET", "/", nil)
84
85 h := wrrPolicy.Select(pool, req, nil)
86 if h != pool[0] {
87 t.Error("Expected first weighted round robin host to be first host in the pool.")
88 }
89 h = wrrPolicy.Select(pool, req, nil)
90 if h != pool[0] {
91 t.Error("Expected second weighted round robin host to be first host in the pool.")
92 }
93 // Third selected host is 1, because counter starts at 0
94 // and increments before host is selected
95 h = wrrPolicy.Select(pool, req, nil)
96 if h != pool[1] {
97 t.Error("Expected third weighted round robin host to be second host in the pool.")
98 }
99 h = wrrPolicy.Select(pool, req, nil)
100 if h != pool[1] {
101 t.Error("Expected fourth weighted round robin host to be second host in the pool.")
102 }
103 h = wrrPolicy.Select(pool, req, nil)
104 if h != pool[2] {
105 t.Error("Expected fifth weighted round robin host to be third host in the pool.")
106 }
107 h = wrrPolicy.Select(pool, req, nil)
108 if h != pool[0] {
109 t.Error("Expected sixth weighted round robin host to be first host in the pool.")
110 }
111
112 // mark host as down
113 pool[0].setHealthy(false)
114 h = wrrPolicy.Select(pool, req, nil)
115 if h != pool[1] {
116 t.Error("Expected to skip down host.")
117 }
118 // mark host as up
119 pool[0].setHealthy(true)
120
121 h = wrrPolicy.Select(pool, req, nil)
122 if h != pool[0] {
123 t.Error("Expected to select first host on availability.")
124 }
125 // mark host as full
126 pool[1].countRequest(1)
127 pool[1].MaxRequests = 1
128 h = wrrPolicy.Select(pool, req, nil)
129 if h != pool[0] {
130 t.Error("Expected to use the weights of the available hosts.")
131 }
132 }
133
134 func TestWeightedRoundRobinPolicyFewerWeightsThanUpstreams(t *testing.T) {
135 pool := testPool()
136 wrrPolicy := WeightedRoundRobinSelection{
137 Weights: []int{1, 1},
138 totalWeight: 2,
139 }
140 req, _ := http.NewRequest("GET", "/", nil)
141
142 pool[0].setHealthy(false)
143 for i := 0; i < 4; i++ {
144 h := wrrPolicy.Select(pool, req, nil)
145 if h != pool[1] {
146 t.Fatalf("Selection %d: got %v, want the only available weighted host %v", i+1, h, pool[1])
147 }
148 }
149 }
150
151 func TestWeightedRoundRobinPolicySingleWeight(t *testing.T) {
152 pool := testPool()
153 wrrPolicy := WeightedRoundRobinSelection{
154 Weights: []int{1},
155 totalWeight: 1,
156 }
157 req, _ := http.NewRequest("GET", "/", nil)
158
159 if h := wrrPolicy.Select(pool, req, nil); h != pool[0] {
160 t.Errorf("got %v, want the only weighted host %v", h, pool[0])
161 }
162
163 pool[0].setHealthy(false)
164 if h := wrrPolicy.Select(pool, req, nil); h != nil {
165 t.Errorf("got %v, want nil when the only weighted host is unavailable", h)
166 }
167 }
168
169 func TestWeightedRoundRobinSelection_Validate(t *testing.T) {
170 tests := []struct {
171 name string
172 weights []int
173 wantErr bool
174 }{
175 {
176 name: "Valid 0 2 1 case",
177 weights: []int{0, 2, 1},
178 wantErr: false,
179 },
180 {
181 name: "Invalid 0 case (single)",
182 weights: []int{0},
183 wantErr: true,
184 },
185 {
186 name: "Invalid 0 0 case (multiple)",
187 weights: []int{0, 0},
188 wantErr: true,
189 },
190 {
191 name: "Valid weights",
192 weights: []int{1, 1, 1},
193 wantErr: false,
194 },
195 }
196
197 for _, tt := range tests {
198 t.Run(tt.name, func(t *testing.T) {
199 s := &WeightedRoundRobinSelection{
200 Weights: tt.weights,
201 }
202 _ = s.Provision(caddy.Context{})
203 err := s.Validate()
204 if (err != nil) != tt.wantErr {
205 t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr)
206 }
207 })
208 }
209 }
210
211 func TestWeightedRoundRobinPolicyWithZeroWeight(t *testing.T) {
212 pool := testPool()
213 wrrPolicy := WeightedRoundRobinSelection{
214 Weights: []int{0, 2, 1},
215 totalWeight: 3,
216 }
217 req, _ := http.NewRequest("GET", "/", nil)
218
219 h := wrrPolicy.Select(pool, req, nil)
220 if h != pool[1] {
221 t.Error("Expected first weighted round robin host to be second host in the pool.")
222 }
223
224 h = wrrPolicy.Select(pool, req, nil)
225 if h != pool[2] {
226 t.Error("Expected second weighted round robin host to be third host in the pool.")
227 }
228
229 h = wrrPolicy.Select(pool, req, nil)
230 if h != pool[1] {
231 t.Error("Expected third weighted round robin host to be second host in the pool.")
232 }
233
234 // mark second host as down
235 pool[1].setHealthy(false)
236 h = wrrPolicy.Select(pool, req, nil)
237 if h != pool[2] {
238 t.Error("Expect select next available host.")
239 }
240
241 h = wrrPolicy.Select(pool, req, nil)
242 if h != pool[2] {
243 t.Error("Expect select only available host.")
244 }
245 // mark second host as up
246 pool[1].setHealthy(true)
247
248 h = wrrPolicy.Select(pool, req, nil)
249 if h != pool[1] {
250 t.Error("Expect select first host on availability.")
251 }
252
253 // test next select in full cycle
254 expected := []*Upstream{pool[1], pool[2], pool[1], pool[1], pool[2], pool[1]}
255 for i, want := range expected {
256 got := wrrPolicy.Select(pool, req, nil)
257 if want != got {
258 t.Errorf("Selection %d: got host[%s], want host[%s]", i+1, got, want)
259 }
260 }
261 }
262
263 func TestLeastConnPolicy(t *testing.T) {
264 pool := testPool()
265 lcPolicy := LeastConnSelection{}
266 req, _ := http.NewRequest("GET", "/", nil)
267
268 pool[0].countRequest(10)
269 pool[1].countRequest(10)
270 h := lcPolicy.Select(pool, req, nil)
271 if h != pool[2] {
272 t.Error("Expected least connection host to be third host.")
273 }
274 pool[2].countRequest(100)
275 h = lcPolicy.Select(pool, req, nil)
276 if h != pool[0] && h != pool[1] {
277 t.Error("Expected least connection host to be first or second host.")
278 }
279 }
280
281 func TestIPHashPolicy(t *testing.T) {
282 pool := testPool()
283 ipHash := IPHashSelection{}
284 req, _ := http.NewRequest("GET", "/", nil)
285
286 // We should be able to predict where every request is routed.
287 req.RemoteAddr = "172.0.0.1:80"
288 h := ipHash.Select(pool, req, nil)
289 if h != pool[0] {
290 t.Error("Expected ip hash policy host to be the first host.")
291 }
292 req.RemoteAddr = "172.0.0.2:80"
293 h = ipHash.Select(pool, req, nil)
294 if h != pool[1] {
295 t.Error("Expected ip hash policy host to be the second host.")
296 }
297 req.RemoteAddr = "172.0.0.3:80"
298 h = ipHash.Select(pool, req, nil)
299 if h != pool[0] {
300 t.Error("Expected ip hash policy host to be the first host.")
301 }
302 req.RemoteAddr = "172.0.0.4:80"
303 h = ipHash.Select(pool, req, nil)
304 if h != pool[1] {
305 t.Error("Expected ip hash policy host to be the second host.")
306 }
307
308 // we should get the same results without a port
309 req.RemoteAddr = "172.0.0.1"
310 h = ipHash.Select(pool, req, nil)
311 if h != pool[0] {
312 t.Error("Expected ip hash policy host to be the first host.")
313 }
314 req.RemoteAddr = "172.0.0.2"
315 h = ipHash.Select(pool, req, nil)
316 if h != pool[1] {
317 t.Error("Expected ip hash policy host to be the second host.")
318 }
319 req.RemoteAddr = "172.0.0.3"
320 h = ipHash.Select(pool, req, nil)
321 if h != pool[0] {
322 t.Error("Expected ip hash policy host to be the first host.")
323 }
324 req.RemoteAddr = "172.0.0.4"
325 h = ipHash.Select(pool, req, nil)
326 if h != pool[1] {
327 t.Error("Expected ip hash policy host to be the second host.")
328 }
329
330 // we should get a healthy host if the original host is unhealthy and a
331 // healthy host is available
332 req.RemoteAddr = "172.0.0.4"
333 pool[1].setHealthy(false)
334 h = ipHash.Select(pool, req, nil)
335 if h != pool[2] {
336 t.Error("Expected ip hash policy host to be the third host.")
337 }
338
339 req.RemoteAddr = "172.0.0.2"
340 h = ipHash.Select(pool, req, nil)
341 if h != pool[0] {
342 t.Error("Expected ip hash policy host to be the first host.")
343 }
344 pool[1].setHealthy(true)
345
346 req.RemoteAddr = "172.0.0.3"
347 pool[2].setHealthy(false)
348 h = ipHash.Select(pool, req, nil)
349 if h != pool[0] {
350 t.Error("Expected ip hash policy host to be the first host.")
351 }
352 req.RemoteAddr = "172.0.0.4"
353 h = ipHash.Select(pool, req, nil)
354 if h != pool[1] {
355 t.Error("Expected ip hash policy host to be the second host.")
356 }
357
358 // We should be able to resize the host pool and still be able to predict
359 // where a req will be routed with the same IP's used above
360 pool = UpstreamPool{
361 {Host: new(Host), Dial: "0.0.0.2"},
362 {Host: new(Host), Dial: "0.0.0.3"},
363 }
364 req.RemoteAddr = "172.0.0.1:80"
365 h = ipHash.Select(pool, req, nil)
366 if h != pool[0] {
367 t.Error("Expected ip hash policy host to be the first host.")
368 }
369 req.RemoteAddr = "172.0.0.2:80"
370 h = ipHash.Select(pool, req, nil)
371 if h != pool[0] {
372 t.Error("Expected ip hash policy host to be the first host.")
373 }
374 req.RemoteAddr = "172.0.0.3:80"
375 h = ipHash.Select(pool, req, nil)
376 if h != pool[0] {
377 t.Error("Expected ip hash policy host to be the first host.")
378 }
379 req.RemoteAddr = "172.0.0.4:80"
380 h = ipHash.Select(pool, req, nil)
381 if h != pool[0] {
382 t.Error("Expected ip hash policy host to be the first host.")
383 }
384
385 // We should get nil when there are no healthy hosts
386 pool[0].setHealthy(false)
387 pool[1].setHealthy(false)
388 h = ipHash.Select(pool, req, nil)
389 if h != nil {
390 t.Error("Expected ip hash policy host to be nil.")
391 }
392
393 // Reproduce #4135
394 pool = UpstreamPool{
395 {Host: new(Host)},
396 {Host: new(Host)},
397 {Host: new(Host)},
398 {Host: new(Host)},
399 {Host: new(Host)},
400 {Host: new(Host)},
401 {Host: new(Host)},
402 {Host: new(Host)},
403 {Host: new(Host)},
404 }
405 pool[0].setHealthy(false)
406 pool[1].setHealthy(false)
407 pool[2].setHealthy(false)
408 pool[3].setHealthy(false)
409 pool[4].setHealthy(false)
410 pool[5].setHealthy(false)
411 pool[6].setHealthy(false)
412 pool[7].setHealthy(false)
413 pool[8].setHealthy(true)
414
415 // We should get a result back when there is one healthy host left.
416 h = ipHash.Select(pool, req, nil)
417 if h == nil {
418 // If it is nil, it means we missed a host even though one is available
419 t.Error("Expected ip hash policy host to not be nil, but it is nil.")
420 }
421 }
422
423 func TestClientIPHashPolicy(t *testing.T) {
424 pool := testPool()
425 ipHash := ClientIPHashSelection{}
426 req, _ := http.NewRequest("GET", "/", nil)
427 req = req.WithContext(context.WithValue(req.Context(), caddyhttp.VarsCtxKey, make(map[string]any)))
428
429 // We should be able to predict where every request is routed.
430 caddyhttp.SetVar(req.Context(), caddyhttp.ClientIPVarKey, "172.0.0.1:80")
431 h := ipHash.Select(pool, req, nil)
432 if h != pool[0] {
433 t.Error("Expected ip hash policy host to be the first host.")
434 }
435 caddyhttp.SetVar(req.Context(), caddyhttp.ClientIPVarKey, "172.0.0.2:80")
436 h = ipHash.Select(pool, req, nil)
437 if h != pool[1] {
438 t.Error("Expected ip hash policy host to be the second host.")
439 }
440 caddyhttp.SetVar(req.Context(), caddyhttp.ClientIPVarKey, "172.0.0.3:80")
441 h = ipHash.Select(pool, req, nil)
442 if h != pool[0] {
443 t.Error("Expected ip hash policy host to be the first host.")
444 }
445 caddyhttp.SetVar(req.Context(), caddyhttp.ClientIPVarKey, "172.0.0.4:80")
446 h = ipHash.Select(pool, req, nil)
447 if h != pool[1] {
448 t.Error("Expected ip hash policy host to be the second host.")
449 }
450
451 // we should get the same results without a port
452 caddyhttp.SetVar(req.Context(), caddyhttp.ClientIPVarKey, "172.0.0.1")
453 h = ipHash.Select(pool, req, nil)
454 if h != pool[0] {
455 t.Error("Expected ip hash policy host to be the first host.")
456 }
457 caddyhttp.SetVar(req.Context(), caddyhttp.ClientIPVarKey, "172.0.0.2")
458 h = ipHash.Select(pool, req, nil)
459 if h != pool[1] {
460 t.Error("Expected ip hash policy host to be the second host.")
461 }
462 caddyhttp.SetVar(req.Context(), caddyhttp.ClientIPVarKey, "172.0.0.3")
463 h = ipHash.Select(pool, req, nil)
464 if h != pool[0] {
465 t.Error("Expected ip hash policy host to be the first host.")
466 }
467 caddyhttp.SetVar(req.Context(), caddyhttp.ClientIPVarKey, "172.0.0.4")
468 h = ipHash.Select(pool, req, nil)
469 if h != pool[1] {
470 t.Error("Expected ip hash policy host to be the second host.")
471 }
472
473 // we should get a healthy host if the original host is unhealthy and a
474 // healthy host is available
475 caddyhttp.SetVar(req.Context(), caddyhttp.ClientIPVarKey, "172.0.0.4")
476 pool[1].setHealthy(false)
477 h = ipHash.Select(pool, req, nil)
478 if h != pool[2] {
479 t.Error("Expected ip hash policy host to be the third host.")
480 }
481
482 caddyhttp.SetVar(req.Context(), caddyhttp.ClientIPVarKey, "172.0.0.2")
483 h = ipHash.Select(pool, req, nil)
484 if h != pool[0] {
485 t.Error("Expected ip hash policy host to be the first host.")
486 }
487 pool[1].setHealthy(true)
488
489 caddyhttp.SetVar(req.Context(), caddyhttp.ClientIPVarKey, "172.0.0.3")
490 pool[2].setHealthy(false)
491 h = ipHash.Select(pool, req, nil)
492 if h != pool[0] {
493 t.Error("Expected ip hash policy host to be the first host.")
494 }
495 caddyhttp.SetVar(req.Context(), caddyhttp.ClientIPVarKey, "172.0.0.4")
496 h = ipHash.Select(pool, req, nil)
497 if h != pool[1] {
498 t.Error("Expected ip hash policy host to be the second host.")
499 }
500
501 // We should be able to resize the host pool and still be able to predict
502 // where a req will be routed with the same IP's used above
503 pool = UpstreamPool{
504 {Host: new(Host), Dial: "0.0.0.2"},
505 {Host: new(Host), Dial: "0.0.0.3"},
506 }
507 caddyhttp.SetVar(req.Context(), caddyhttp.ClientIPVarKey, "172.0.0.1:80")
508 h = ipHash.Select(pool, req, nil)
509 if h != pool[0] {
510 t.Error("Expected ip hash policy host to be the first host.")
511 }
512 caddyhttp.SetVar(req.Context(), caddyhttp.ClientIPVarKey, "172.0.0.2:80")
513 h = ipHash.Select(pool, req, nil)
514 if h != pool[0] {
515 t.Error("Expected ip hash policy host to be the first host.")
516 }
517 caddyhttp.SetVar(req.Context(), caddyhttp.ClientIPVarKey, "172.0.0.3:80")
518 h = ipHash.Select(pool, req, nil)
519 if h != pool[0] {
520 t.Error("Expected ip hash policy host to be the first host.")
521 }
522 caddyhttp.SetVar(req.Context(), caddyhttp.ClientIPVarKey, "172.0.0.4:80")
523 h = ipHash.Select(pool, req, nil)
524 if h != pool[0] {
525 t.Error("Expected ip hash policy host to be the first host.")
526 }
527
528 // We should get nil when there are no healthy hosts
529 pool[0].setHealthy(false)
530 pool[1].setHealthy(false)
531 h = ipHash.Select(pool, req, nil)
532 if h != nil {
533 t.Error("Expected ip hash policy host to be nil.")
534 }
535
536 // Reproduce #4135
537 pool = UpstreamPool{
538 {Host: new(Host)},
539 {Host: new(Host)},
540 {Host: new(Host)},
541 {Host: new(Host)},
542 {Host: new(Host)},
543 {Host: new(Host)},
544 {Host: new(Host)},
545 {Host: new(Host)},
546 {Host: new(Host)},
547 }
548 pool[0].setHealthy(false)
549 pool[1].setHealthy(false)
550 pool[2].setHealthy(false)
551 pool[3].setHealthy(false)
552 pool[4].setHealthy(false)
553 pool[5].setHealthy(false)
554 pool[6].setHealthy(false)
555 pool[7].setHealthy(false)
556 pool[8].setHealthy(true)
557
558 // We should get a result back when there is one healthy host left.
559 h = ipHash.Select(pool, req, nil)
560 if h == nil {
561 // If it is nil, it means we missed a host even though one is available
562 t.Error("Expected ip hash policy host to not be nil, but it is nil.")
563 }
564 }
565
566 func TestFirstPolicy(t *testing.T) {
567 pool := testPool()
568 firstPolicy := FirstSelection{}
569 req := httptest.NewRequest(http.MethodGet, "/", nil)
570
571 h := firstPolicy.Select(pool, req, nil)
572 if h != pool[0] {
573 t.Error("Expected first policy host to be the first host.")
574 }
575
576 pool[0].setHealthy(false)
577 h = firstPolicy.Select(pool, req, nil)
578 if h != pool[1] {
579 t.Error("Expected first policy host to be the second host.")
580 }
581 }
582
583 func TestQueryHashPolicy(t *testing.T) {
584 ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
585 defer cancel()
586 queryPolicy := QueryHashSelection{Key: "foo"}
587 if err := queryPolicy.Provision(ctx); err != nil {
588 t.Errorf("Provision error: %v", err)
589 t.FailNow()
590 }
591
592 pool := testPool()
593
594 request := httptest.NewRequest(http.MethodGet, "/?foo=1", nil)
595 h := queryPolicy.Select(pool, request, nil)
596 if h != pool[0] {
597 t.Error("Expected query policy host to be the first host.")
598 }
599
600 request = httptest.NewRequest(http.MethodGet, "/?foo=100000", nil)
601 h = queryPolicy.Select(pool, request, nil)
602 if h != pool[1] {
603 t.Error("Expected query policy host to be the second host.")
604 }
605
606 request = httptest.NewRequest(http.MethodGet, "/?foo=1", nil)
607 pool[0].setHealthy(false)
608 h = queryPolicy.Select(pool, request, nil)
609 if h != pool[2] {
610 t.Error("Expected query policy host to be the third host.")
611 }
612
613 request = httptest.NewRequest(http.MethodGet, "/?foo=100000", nil)
614 h = queryPolicy.Select(pool, request, nil)
615 if h != pool[1] {
616 t.Error("Expected query policy host to be the second host.")
617 }
618
619 // We should be able to resize the host pool and still be able to predict
620 // where a request will be routed with the same query used above
621 pool = UpstreamPool{
622 {Host: new(Host)},
623 {Host: new(Host)},
624 }
625
626 request = httptest.NewRequest(http.MethodGet, "/?foo=1", nil)
627 h = queryPolicy.Select(pool, request, nil)
628 if h != pool[0] {
629 t.Error("Expected query policy host to be the first host.")
630 }
631
632 pool[0].setHealthy(false)
633 h = queryPolicy.Select(pool, request, nil)
634 if h != pool[1] {
635 t.Error("Expected query policy host to be the second host.")
636 }
637
638 request = httptest.NewRequest(http.MethodGet, "/?foo=4", nil)
639 h = queryPolicy.Select(pool, request, nil)
640 if h != pool[1] {
641 t.Error("Expected query policy host to be the second host.")
642 }
643
644 pool[0].setHealthy(false)
645 pool[1].setHealthy(false)
646 h = queryPolicy.Select(pool, request, nil)
647 if h != nil {
648 t.Error("Expected query policy host to be nil.")
649 }
650
651 request = httptest.NewRequest(http.MethodGet, "/?foo=aa11&foo=bb22", nil)
652 pool = testPool()
653 h = queryPolicy.Select(pool, request, nil)
654 if h != pool[0] {
655 t.Error("Expected query policy host to be the first host.")
656 }
657 }
658
659 func TestURIHashPolicy(t *testing.T) {
660 pool := testPool()
661 uriPolicy := URIHashSelection{}
662
663 request := httptest.NewRequest(http.MethodGet, "/test", nil)
664 h := uriPolicy.Select(pool, request, nil)
665 if h != pool[2] {
666 t.Error("Expected uri policy host to be the third host.")
667 }
668
669 pool[2].setHealthy(false)
670 h = uriPolicy.Select(pool, request, nil)
671 if h != pool[0] {
672 t.Error("Expected uri policy host to be the first host.")
673 }
674
675 request = httptest.NewRequest(http.MethodGet, "/test_2", nil)
676 h = uriPolicy.Select(pool, request, nil)
677 if h != pool[0] {
678 t.Error("Expected uri policy host to be the first host.")
679 }
680
681 // We should be able to resize the host pool and still be able to predict
682 // where a request will be routed with the same URI's used above
683 pool = UpstreamPool{
684 {Host: new(Host)},
685 {Host: new(Host)},
686 }
687
688 request = httptest.NewRequest(http.MethodGet, "/test", nil)
689 h = uriPolicy.Select(pool, request, nil)
690 if h != pool[0] {
691 t.Error("Expected uri policy host to be the first host.")
692 }
693
694 pool[0].setHealthy(false)
695 h = uriPolicy.Select(pool, request, nil)
696 if h != pool[1] {
697 t.Error("Expected uri policy host to be the first host.")
698 }
699
700 request = httptest.NewRequest(http.MethodGet, "/test_2", nil)
701 h = uriPolicy.Select(pool, request, nil)
702 if h != pool[1] {
703 t.Error("Expected uri policy host to be the second host.")
704 }
705
706 pool[0].setHealthy(false)
707 pool[1].setHealthy(false)
708 h = uriPolicy.Select(pool, request, nil)
709 if h != nil {
710 t.Error("Expected uri policy host to be nil.")
711 }
712 }
713
714 func TestLeastRequests(t *testing.T) {
715 pool := testPool()
716 pool[0].Dial = "localhost:8080"
717 pool[1].Dial = "localhost:8081"
718 pool[2].Dial = "localhost:8082"
719 pool[0].setHealthy(true)
720 pool[1].setHealthy(true)
721 pool[2].setHealthy(true)
722 pool[0].countRequest(10)
723 pool[1].countRequest(20)
724 pool[2].countRequest(30)
725
726 result := leastRequests(pool)
727
728 if result == nil {
729 t.Error("Least request should not return nil")
730 }
731
732 if result != pool[0] {
733 t.Error("Least request should return pool[0]")
734 }
735 }
736
737 func TestLeastRequestsAllBusy(t *testing.T) {
738 pool := testPool()
739 pool[0].countRequest(30)
740 pool[1].countRequest(1)
741 pool[2].countRequest(20)
742
743 for i := 0; i < 100; i++ {
744 if result := leastRequests(pool); result != pool[1] {
745 t.Fatalf("iteration %d: got %v, want the least loaded host %v", i, result, pool[1])
746 }
747 }
748 }
749
750 func TestRandomChoicePolicy(t *testing.T) {
751 pool := testPool()
752 pool[0].Dial = "localhost:8080"
753 pool[1].Dial = "localhost:8081"
754 pool[2].Dial = "localhost:8082"
755 pool[0].setHealthy(false)
756 pool[1].setHealthy(true)
757 pool[2].setHealthy(true)
758 pool[0].countRequest(10)
759 pool[1].countRequest(20)
760 pool[2].countRequest(30)
761
762 request := httptest.NewRequest(http.MethodGet, "/test", nil)
763 randomChoicePolicy := RandomChoiceSelection{Choose: 2}
764
765 h := randomChoicePolicy.Select(pool, request, nil)
766
767 if h == nil {
768 t.Error("RandomChoicePolicy should not return nil")
769 }
770
771 if h == pool[0] {
772 t.Error("RandomChoicePolicy should not choose pool[0]")
773 }
774 }
775
776 func TestRandomChoicePolicyLeastLoaded(t *testing.T) {
777 // when the number of available upstreams does not exceed the choose
778 // count, all of them must be candidates, so the least-loaded one
779 // must always be selected; the pool intentionally starts with an
780 // unavailable upstream to verify that reservoir sampling counts
781 // available upstreams rather than pool indices
782 pool := testPool()
783 pool[0].Dial = "localhost:8080"
784 pool[1].Dial = "localhost:8081"
785 pool[2].Dial = "localhost:8082"
786 pool[0].setHealthy(false)
787 pool[1].setHealthy(true)
788 pool[2].setHealthy(true)
789 pool[1].countRequest(30)
790 // pool[2] has no active requests
791
792 request := httptest.NewRequest(http.MethodGet, "/test", nil)
793 randomChoicePolicy := RandomChoiceSelection{Choose: 2}
794
795 for i := 0; i < 100; i++ {
796 h := randomChoicePolicy.Select(pool, request, nil)
797 if h != pool[2] {
798 t.Fatalf("with 2 available upstreams and choose=2, the least-loaded upstream (pool[2]) must always be selected; got %v on iteration %d", h, i)
799 }
800 }
801 }
802
803 func TestCookieHashPolicy(t *testing.T) {
804 ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
805 defer cancel()
806 cookieHashPolicy := CookieHashSelection{}
807 if err := cookieHashPolicy.Provision(ctx); err != nil {
808 t.Errorf("Provision error: %v", err)
809 t.FailNow()
810 }
811
812 pool := testPool()
813 pool[0].Dial = "localhost:8080"
814 pool[1].Dial = "localhost:8081"
815 pool[2].Dial = "localhost:8082"
816 pool[0].setHealthy(true)
817 pool[1].setHealthy(false)
818 pool[2].setHealthy(false)
819 request := httptest.NewRequest(http.MethodGet, "/test", nil)
820 w := httptest.NewRecorder()
821
822 h := cookieHashPolicy.Select(pool, request, w)
823 cookieServer1 := w.Result().Cookies()[0]
824 if cookieServer1 == nil {
825 t.Fatal("cookieHashPolicy should set a cookie")
826 }
827 if cookieServer1.Name != "lb" {
828 t.Error("cookieHashPolicy should set a cookie with name lb")
829 }
830 if cookieServer1.Secure {
831 t.Error("cookieHashPolicy should set cookie Secure attribute to false when request is not secure")
832 }
833 if h != pool[0] {
834 t.Error("Expected cookieHashPolicy host to be the first only available host.")
835 }
836 pool[1].setHealthy(true)
837 pool[2].setHealthy(true)
838 request = httptest.NewRequest(http.MethodGet, "/test", nil)
839 w = httptest.NewRecorder()
840 request.AddCookie(cookieServer1)
841 h = cookieHashPolicy.Select(pool, request, w)
842 if h != pool[0] {
843 t.Error("Expected cookieHashPolicy host to stick to the first host (matching cookie).")
844 }
845 s := w.Result().Cookies()
846 if len(s) != 0 {
847 t.Error("Expected cookieHashPolicy to not set a new cookie.")
848 }
849 pool[0].setHealthy(false)
850 request = httptest.NewRequest(http.MethodGet, "/test", nil)
851 w = httptest.NewRecorder()
852 request.AddCookie(cookieServer1)
853 h = cookieHashPolicy.Select(pool, request, w)
854 if h == pool[0] {
855 t.Error("Expected cookieHashPolicy to select a new host.")
856 }
857 if w.Result().Cookies() == nil {
858 t.Error("Expected cookieHashPolicy to set a new cookie.")
859 }
860 }
861
862 func TestCookieHashPolicyWithSecureRequest(t *testing.T) {
863 ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
864 defer cancel()
865 cookieHashPolicy := CookieHashSelection{}
866 if err := cookieHashPolicy.Provision(ctx); err != nil {
867 t.Errorf("Provision error: %v", err)
868 t.FailNow()
869 }
870
871 pool := testPool()
872 pool[0].Dial = "localhost:8080"
873 pool[1].Dial = "localhost:8081"
874 pool[2].Dial = "localhost:8082"
875 pool[0].setHealthy(true)
876 pool[1].setHealthy(false)
877 pool[2].setHealthy(false)
878
879 // Create a test server that serves HTTPS requests
880 ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
881 h := cookieHashPolicy.Select(pool, r, w)
882 if h != pool[0] {
883 t.Error("Expected cookieHashPolicy host to be the first only available host.")
884 }
885 }))
886 defer ts.Close()
887
888 // Make a new HTTPS request to the test server
889 client := ts.Client()
890 request, err := http.NewRequest(http.MethodGet, ts.URL+"/test", nil)
891 if err != nil {
892 t.Fatal(err)
893 }
894 response, err := client.Do(request)
895 if err != nil {
896 t.Fatal(err)
897 }
898
899 // Check if the cookie set is Secure and has SameSiteNone mode
900 cookies := response.Cookies()
901 if len(cookies) == 0 {
902 t.Fatal("Expected a cookie to be set")
903 }
904 cookie := cookies[0]
905 if !cookie.Secure {
906 t.Error("Expected cookie Secure attribute to be true when request is secure")
907 }
908 if cookie.SameSite != http.SameSiteNoneMode {
909 t.Error("Expected cookie SameSite attribute to be None when request is secure")
910 }
911 }
912
913 func TestCookieHashPolicyWithFirstFallback(t *testing.T) {
914 ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
915 defer cancel()
916 cookieHashPolicy := CookieHashSelection{
917 FallbackRaw: caddyconfig.JSONModuleObject(FirstSelection{}, "policy", "first", nil),
918 }
919 if err := cookieHashPolicy.Provision(ctx); err != nil {
920 t.Errorf("Provision error: %v", err)
921 t.FailNow()
922 }
923
924 pool := testPool()
925 pool[0].Dial = "localhost:8080"
926 pool[1].Dial = "localhost:8081"
927 pool[2].Dial = "localhost:8082"
928 pool[0].setHealthy(true)
929 pool[1].setHealthy(true)
930 pool[2].setHealthy(true)
931 request := httptest.NewRequest(http.MethodGet, "/test", nil)
932 w := httptest.NewRecorder()
933
934 h := cookieHashPolicy.Select(pool, request, w)
935 cookieServer1 := w.Result().Cookies()[0]
936 if cookieServer1 == nil {
937 t.Fatal("cookieHashPolicy should set a cookie")
938 }
939 if cookieServer1.Name != "lb" {
940 t.Error("cookieHashPolicy should set a cookie with name lb")
941 }
942 if h != pool[0] {
943 t.Errorf("Expected cookieHashPolicy host to be the first only available host, got %s", h)
944 }
945 request = httptest.NewRequest(http.MethodGet, "/test", nil)
946 w = httptest.NewRecorder()
947 request.AddCookie(cookieServer1)
948 h = cookieHashPolicy.Select(pool, request, w)
949 if h != pool[0] {
950 t.Errorf("Expected cookieHashPolicy host to stick to the first host (matching cookie), got %s", h)
951 }
952 s := w.Result().Cookies()
953 if len(s) != 0 {
954 t.Error("Expected cookieHashPolicy to not set a new cookie.")
955 }
956 pool[0].setHealthy(false)
957 request = httptest.NewRequest(http.MethodGet, "/test", nil)
958 w = httptest.NewRecorder()
959 request.AddCookie(cookieServer1)
960 h = cookieHashPolicy.Select(pool, request, w)
961 if h != pool[1] {
962 t.Errorf("Expected cookieHashPolicy to select the next first available host, got %s", h)
963 }
964 if w.Result().Cookies() == nil {
965 t.Error("Expected cookieHashPolicy to set a new cookie.")
966 }
967 }
968
969 func TestCookieHashPolicyWithSecret(t *testing.T) {
970 ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
971 defer cancel()
972 cookieHashPolicy := CookieHashSelection{Secret: "hunter2"}
973 if err := cookieHashPolicy.Provision(ctx); err != nil {
974 t.Errorf("Provision error: %v", err)
975 t.FailNow()
976 }
977
978 pool := testPool()
979 pool[0].Dial = "localhost:8080"
980 pool[1].Dial = "localhost:8081"
981 pool[2].Dial = "localhost:8082"
982 pool[0].setHealthy(true)
983 pool[1].setHealthy(true)
984 pool[2].setHealthy(true)
985
986 request := httptest.NewRequest(http.MethodGet, "/test", nil)
987 w := httptest.NewRecorder()
988 h := cookieHashPolicy.Select(pool, request, w)
989 cookie := w.Result().Cookies()[0]
990
991 // a matching cookie sticks to the same host
992 request = httptest.NewRequest(http.MethodGet, "/test", nil)
993 w = httptest.NewRecorder()
994 request.AddCookie(cookie)
995 if got := cookieHashPolicy.Select(pool, request, w); got != h {
996 t.Errorf("Expected to stick to host %s, got %s", h, got)
997 }
998 if len(w.Result().Cookies()) != 0 {
999 t.Error("Expected no new cookie for a matching value")
1000 }
1001
1002 // a tampered cookie value must not match any host and gets a fresh cookie
1003 request = httptest.NewRequest(http.MethodGet, "/test", nil)
1004 w = httptest.NewRecorder()
1005 request.AddCookie(&http.Cookie{Name: cookie.Name, Value: cookie.Value[:len(cookie.Value)-1] + "0"})
1006 cookieHashPolicy.Select(pool, request, w)
1007 if len(w.Result().Cookies()) == 0 {
1008 t.Error("Expected a new cookie to be set for a non-matching value")
1009 }
1010 }
1011
1012 func TestWeightedRoundRobinPolicyAvailableWeights(t *testing.T) {
1013 pool := testPool()
1014 pool[1].setHealthy(false)
1015 policy := WeightedRoundRobinSelection{Weights: []int{1, 8, 3}, totalWeight: 12}
1016 counts := make(map[*Upstream]int)
1017 for i := 0; i < 12; i++ {
1018 counts[policy.Select(pool, nil, nil)]++
1019 }
1020 if counts[pool[0]] != 3 || counts[pool[2]] != 9 || len(counts) != 2 {
1021 t.Fatalf("Expected a 1:3 distribution among available hosts, got %v", counts)
1022 }
1023 }