@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 913 lines 26.2 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 "crypto/hmac"
19 "crypto/sha256"
20 "encoding/hex"
21 "encoding/json"
22 "fmt"
23 weakrand "math/rand/v2"
24 "net"
25 "net/http"
26 "strconv"
27 "strings"
28 "sync/atomic"
29 "time"
30
31 "github.com/cespare/xxhash/v2"
32
33 "github.com/caddyserver/caddy/v2"
34 "github.com/caddyserver/caddy/v2/caddyconfig"
35 "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
36 "github.com/caddyserver/caddy/v2/modules/caddyhttp"
37 )
38
39 func init() {
40 caddy.RegisterModule(RandomSelection{})
41 caddy.RegisterModule(RandomChoiceSelection{})
42 caddy.RegisterModule(LeastConnSelection{})
43 caddy.RegisterModule(new(RoundRobinSelection))
44 caddy.RegisterModule(new(WeightedRoundRobinSelection))
45 caddy.RegisterModule(FirstSelection{})
46 caddy.RegisterModule(IPHashSelection{})
47 caddy.RegisterModule(ClientIPHashSelection{})
48 caddy.RegisterModule(URIHashSelection{})
49 caddy.RegisterModule(QueryHashSelection{})
50 caddy.RegisterModule(HeaderHashSelection{})
51 caddy.RegisterModule(CookieHashSelection{})
52 }
53
54 // RandomSelection is a policy that selects
55 // an available host at random.
56 type RandomSelection struct{}
57
58 // CaddyModule returns the Caddy module information.
59 func (RandomSelection) CaddyModule() caddy.ModuleInfo {
60 return caddy.ModuleInfo{
61 ID: "http.reverse_proxy.selection_policies.random",
62 New: func() caddy.Module { return new(RandomSelection) },
63 }
64 }
65
66 // Select returns an available host, if any.
67 func (r RandomSelection) Select(pool UpstreamPool, request *http.Request, _ http.ResponseWriter) *Upstream {
68 return selectRandomHost(pool)
69 }
70
71 // UnmarshalCaddyfile sets up the module from Caddyfile tokens.
72 func (r *RandomSelection) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
73 d.Next() // consume policy name
74 if d.NextArg() {
75 return d.ArgErr()
76 }
77 return nil
78 }
79
80 // WeightedRoundRobinSelection is a policy that selects
81 // a host based on weighted round-robin ordering.
82 type WeightedRoundRobinSelection struct {
83 // The weight of each upstream in order,
84 // corresponding with the list of upstreams configured.
85 Weights []int `json:"weights,omitempty"`
86 index atomic.Uint32
87 totalWeight int
88 }
89
90 // CaddyModule returns the Caddy module information.
91 func (*WeightedRoundRobinSelection) CaddyModule() caddy.ModuleInfo {
92 return caddy.ModuleInfo{
93 ID: "http.reverse_proxy.selection_policies.weighted_round_robin",
94 New: func() caddy.Module {
95 return new(WeightedRoundRobinSelection)
96 },
97 }
98 }
99
100 // UnmarshalCaddyfile sets up the module from Caddyfile tokens.
101 func (r *WeightedRoundRobinSelection) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
102 d.Next() // consume policy name
103
104 args := d.RemainingArgs()
105 if len(args) == 0 {
106 return d.ArgErr()
107 }
108
109 for _, weight := range args {
110 weightInt, err := strconv.Atoi(weight)
111 if err != nil {
112 return d.Errf("invalid weight value '%s': %v", weight, err)
113 }
114 if weightInt < 0 {
115 return d.Errf("invalid weight value '%s': weight should be non-negative", weight)
116 }
117 r.Weights = append(r.Weights, weightInt)
118 }
119 return nil
120 }
121
122 // Provision sets up r.
123 func (r *WeightedRoundRobinSelection) Provision(ctx caddy.Context) error {
124 for _, weight := range r.Weights {
125 r.totalWeight += weight
126 }
127 return nil
128 }
129
130 // Validate ensures that r's configuration is valid
131 func (r *WeightedRoundRobinSelection) Validate() error {
132 if r.totalWeight <= 0 {
133 return fmt.Errorf("weighted_round_robin requires at least one upstream with a positive weight")
134 }
135 for _, weight := range r.Weights {
136 if weight < 0 {
137 return fmt.Errorf("weight of an upstream cannot be negative")
138 }
139 }
140 return nil
141 }
142
143 // Select returns an available host, if any.
144 func (r *WeightedRoundRobinSelection) Select(pool UpstreamPool, _ *http.Request, _ http.ResponseWriter) *Upstream {
145 type weightedUpstream struct {
146 upstream *Upstream
147 weight int
148 }
149 upstreams := make([]weightedUpstream, 0, len(pool))
150 var totalWeight int
151 for i, upstream := range pool {
152 if i >= len(r.Weights) || r.Weights[i] <= 0 || !upstream.Available() {
153 continue
154 }
155 upstreams = append(upstreams, weightedUpstream{upstream, r.Weights[i]})
156 totalWeight += r.Weights[i]
157 }
158 if totalWeight == 0 {
159 return nil
160 }
161
162 currentWeight := uint64(r.index.Add(1)) % uint64(totalWeight)
163 for _, upstream := range upstreams {
164 if currentWeight < uint64(upstream.weight) {
165 return upstream.upstream
166 }
167 currentWeight -= uint64(upstream.weight)
168 }
169 return nil
170 }
171
172 // RandomChoiceSelection is a policy that selects
173 // two or more available hosts at random, then
174 // chooses the one with the least load.
175 type RandomChoiceSelection struct {
176 // The size of the sub-pool created from the larger upstream pool. The default value
177 // is 2 and the maximum at selection time is the size of the upstream pool.
178 Choose int `json:"choose,omitempty"`
179 }
180
181 // CaddyModule returns the Caddy module information.
182 func (RandomChoiceSelection) CaddyModule() caddy.ModuleInfo {
183 return caddy.ModuleInfo{
184 ID: "http.reverse_proxy.selection_policies.random_choose",
185 New: func() caddy.Module { return new(RandomChoiceSelection) },
186 }
187 }
188
189 // UnmarshalCaddyfile sets up the module from Caddyfile tokens.
190 func (r *RandomChoiceSelection) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
191 d.Next() // consume policy name
192
193 if !d.NextArg() {
194 return d.ArgErr()
195 }
196 chooseStr := d.Val()
197 choose, err := strconv.Atoi(chooseStr)
198 if err != nil {
199 return d.Errf("invalid choice value '%s': %v", chooseStr, err)
200 }
201 r.Choose = choose
202 return nil
203 }
204
205 // Provision sets up r.
206 func (r *RandomChoiceSelection) Provision(ctx caddy.Context) error {
207 if r.Choose == 0 {
208 r.Choose = 2
209 }
210 return nil
211 }
212
213 // Validate ensures that r's configuration is valid.
214 func (r RandomChoiceSelection) Validate() error {
215 if r.Choose < 2 {
216 return fmt.Errorf("choose must be at least 2")
217 }
218 return nil
219 }
220
221 // Select returns an available host, if any.
222 func (r RandomChoiceSelection) Select(pool UpstreamPool, _ *http.Request, _ http.ResponseWriter) *Upstream {
223 k := min(r.Choose, len(pool))
224
225 // reservoir sampling (Algorithm R) over the available upstreams:
226 // the first k available upstreams fill the reservoir, then each
227 // subsequent one replaces a random reservoir entry with probability
228 // k/n, so every available upstream is sampled uniformly
229 choices := make([]*Upstream, 0, k)
230 var available int
231 for _, upstream := range pool {
232 if !upstream.Available() {
233 continue
234 }
235 available++
236 if len(choices) < k {
237 choices = append(choices, upstream)
238 continue
239 }
240 j := weakrand.IntN(available) //nolint:gosec
241 if j < k {
242 choices[j] = upstream
243 }
244 }
245 return leastRequests(choices)
246 }
247
248 // LeastConnSelection is a policy that selects the
249 // host with the least active requests. If multiple
250 // hosts have the same fewest number, one is chosen
251 // randomly. The term "conn" or "connection" is used
252 // in this policy name due to its similar meaning in
253 // other software, but our load balancer actually
254 // counts active requests rather than connections,
255 // since these days requests are multiplexed onto
256 // shared connections.
257 type LeastConnSelection struct{}
258
259 // CaddyModule returns the Caddy module information.
260 func (LeastConnSelection) CaddyModule() caddy.ModuleInfo {
261 return caddy.ModuleInfo{
262 ID: "http.reverse_proxy.selection_policies.least_conn",
263 New: func() caddy.Module { return new(LeastConnSelection) },
264 }
265 }
266
267 // Select selects the up host with the least number of connections in the
268 // pool. If more than one host has the same least number of connections,
269 // one of the hosts is chosen at random.
270 func (LeastConnSelection) Select(pool UpstreamPool, _ *http.Request, _ http.ResponseWriter) *Upstream {
271 var bestHost *Upstream
272 var count int
273 leastReqs := -1
274
275 for _, host := range pool {
276 if !host.Available() {
277 continue
278 }
279 numReqs := host.NumRequests()
280 if leastReqs == -1 || numReqs < leastReqs {
281 leastReqs = numReqs
282 count = 0
283 }
284
285 // among hosts with same least connections, perform a reservoir
286 // sample: https://en.wikipedia.org/wiki/Reservoir_sampling
287 if numReqs == leastReqs {
288 count++
289 if count == 1 || weakrand.IntN(count) == 0 { //nolint:gosec
290 bestHost = host
291 }
292 }
293 }
294
295 return bestHost
296 }
297
298 // UnmarshalCaddyfile sets up the module from Caddyfile tokens.
299 func (r *LeastConnSelection) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
300 d.Next() // consume policy name
301 if d.NextArg() {
302 return d.ArgErr()
303 }
304 return nil
305 }
306
307 // RoundRobinSelection is a policy that selects
308 // a host based on round-robin ordering.
309 type RoundRobinSelection struct {
310 robin atomic.Uint32
311 }
312
313 // CaddyModule returns the Caddy module information.
314 func (*RoundRobinSelection) CaddyModule() caddy.ModuleInfo {
315 return caddy.ModuleInfo{
316 ID: "http.reverse_proxy.selection_policies.round_robin",
317 New: func() caddy.Module { return new(RoundRobinSelection) },
318 }
319 }
320
321 // Select returns an available host, if any.
322 func (r *RoundRobinSelection) Select(pool UpstreamPool, _ *http.Request, _ http.ResponseWriter) *Upstream {
323 n := uint32(len(pool))
324 if n == 0 {
325 return nil
326 }
327 for range n {
328 robin := r.robin.Add(1)
329 host := pool[robin%n]
330 if host.Available() {
331 return host
332 }
333 }
334 return nil
335 }
336
337 // UnmarshalCaddyfile sets up the module from Caddyfile tokens.
338 func (r *RoundRobinSelection) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
339 d.Next() // consume policy name
340 if d.NextArg() {
341 return d.ArgErr()
342 }
343 return nil
344 }
345
346 // FirstSelection is a policy that selects
347 // the first available host.
348 type FirstSelection struct{}
349
350 // CaddyModule returns the Caddy module information.
351 func (FirstSelection) CaddyModule() caddy.ModuleInfo {
352 return caddy.ModuleInfo{
353 ID: "http.reverse_proxy.selection_policies.first",
354 New: func() caddy.Module { return new(FirstSelection) },
355 }
356 }
357
358 // Select returns an available host, if any.
359 func (FirstSelection) Select(pool UpstreamPool, _ *http.Request, _ http.ResponseWriter) *Upstream {
360 for _, host := range pool {
361 if host.Available() {
362 return host
363 }
364 }
365 return nil
366 }
367
368 // UnmarshalCaddyfile sets up the module from Caddyfile tokens.
369 func (r *FirstSelection) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
370 d.Next() // consume policy name
371 if d.NextArg() {
372 return d.ArgErr()
373 }
374 return nil
375 }
376
377 // IPHashSelection is a policy that selects a host
378 // based on hashing the remote IP of the request.
379 type IPHashSelection struct{}
380
381 // CaddyModule returns the Caddy module information.
382 func (IPHashSelection) CaddyModule() caddy.ModuleInfo {
383 return caddy.ModuleInfo{
384 ID: "http.reverse_proxy.selection_policies.ip_hash",
385 New: func() caddy.Module { return new(IPHashSelection) },
386 }
387 }
388
389 // Select returns an available host, if any.
390 func (IPHashSelection) Select(pool UpstreamPool, req *http.Request, _ http.ResponseWriter) *Upstream {
391 clientIP, _, err := net.SplitHostPort(req.RemoteAddr)
392 if err != nil {
393 clientIP = req.RemoteAddr
394 }
395 return hostByHashing(pool, clientIP)
396 }
397
398 // UnmarshalCaddyfile sets up the module from Caddyfile tokens.
399 func (r *IPHashSelection) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
400 d.Next() // consume policy name
401 if d.NextArg() {
402 return d.ArgErr()
403 }
404 return nil
405 }
406
407 // ClientIPHashSelection is a policy that selects a host
408 // based on hashing the client IP of the request, as determined
409 // by the HTTP app's trusted proxies settings.
410 type ClientIPHashSelection struct{}
411
412 // CaddyModule returns the Caddy module information.
413 func (ClientIPHashSelection) CaddyModule() caddy.ModuleInfo {
414 return caddy.ModuleInfo{
415 ID: "http.reverse_proxy.selection_policies.client_ip_hash",
416 New: func() caddy.Module { return new(ClientIPHashSelection) },
417 }
418 }
419
420 // Select returns an available host, if any.
421 func (ClientIPHashSelection) Select(pool UpstreamPool, req *http.Request, _ http.ResponseWriter) *Upstream {
422 address := caddyhttp.GetVar(req.Context(), caddyhttp.ClientIPVarKey).(string)
423 clientIP, _, err := net.SplitHostPort(address)
424 if err != nil {
425 clientIP = address // no port
426 }
427 return hostByHashing(pool, clientIP)
428 }
429
430 // UnmarshalCaddyfile sets up the module from Caddyfile tokens.
431 func (r *ClientIPHashSelection) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
432 d.Next() // consume policy name
433 if d.NextArg() {
434 return d.ArgErr()
435 }
436 return nil
437 }
438
439 // URIHashSelection is a policy that selects a
440 // host by hashing the request URI.
441 type URIHashSelection struct{}
442
443 // CaddyModule returns the Caddy module information.
444 func (URIHashSelection) CaddyModule() caddy.ModuleInfo {
445 return caddy.ModuleInfo{
446 ID: "http.reverse_proxy.selection_policies.uri_hash",
447 New: func() caddy.Module { return new(URIHashSelection) },
448 }
449 }
450
451 // Select returns an available host, if any.
452 func (URIHashSelection) Select(pool UpstreamPool, req *http.Request, _ http.ResponseWriter) *Upstream {
453 return hostByHashing(pool, req.RequestURI)
454 }
455
456 // UnmarshalCaddyfile sets up the module from Caddyfile tokens.
457 func (r *URIHashSelection) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
458 d.Next() // consume policy name
459 if d.NextArg() {
460 return d.ArgErr()
461 }
462 return nil
463 }
464
465 // QueryHashSelection is a policy that selects
466 // a host based on a given request query parameter.
467 type QueryHashSelection struct {
468 // The query key whose value is to be hashed and used for upstream selection.
469 Key string `json:"key,omitempty"`
470
471 // The fallback policy to use if the query key is not present. Defaults to `random`.
472 FallbackRaw json.RawMessage `json:"fallback,omitempty" caddy:"namespace=http.reverse_proxy.selection_policies inline_key=policy"`
473 fallback Selector
474 }
475
476 // CaddyModule returns the Caddy module information.
477 func (QueryHashSelection) CaddyModule() caddy.ModuleInfo {
478 return caddy.ModuleInfo{
479 ID: "http.reverse_proxy.selection_policies.query",
480 New: func() caddy.Module { return new(QueryHashSelection) },
481 }
482 }
483
484 // Provision sets up the module.
485 func (s *QueryHashSelection) Provision(ctx caddy.Context) error {
486 if s.Key == "" {
487 return fmt.Errorf("query key is required")
488 }
489 if s.FallbackRaw == nil {
490 s.FallbackRaw = caddyconfig.JSONModuleObject(RandomSelection{}, "policy", "random", nil)
491 }
492 mod, err := ctx.LoadModule(s, "FallbackRaw")
493 if err != nil {
494 return fmt.Errorf("loading fallback selection policy: %s", err)
495 }
496 s.fallback = mod.(Selector)
497 return nil
498 }
499
500 // Select returns an available host, if any.
501 func (s QueryHashSelection) Select(pool UpstreamPool, req *http.Request, _ http.ResponseWriter) *Upstream {
502 // Since the query may have multiple values for the same key,
503 // we'll join them to avoid a problem where the user can control
504 // the upstream that the request goes to by sending multiple values
505 // for the same key, when the upstream only considers the first value.
506 // Keep in mind that a client changing the order of the values may
507 // affect which upstream is selected, but this is a semantically
508 // different request, because the order of the values is significant.
509 vals := strings.Join(req.URL.Query()[s.Key], ",")
510 if vals == "" {
511 return s.fallback.Select(pool, req, nil)
512 }
513 return hostByHashing(pool, vals)
514 }
515
516 // UnmarshalCaddyfile sets up the module from Caddyfile tokens.
517 func (s *QueryHashSelection) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
518 d.Next() // consume policy name
519
520 if !d.NextArg() {
521 return d.ArgErr()
522 }
523 s.Key = d.Val()
524
525 for d.NextBlock(0) {
526 switch d.Val() {
527 case "fallback":
528 if !d.NextArg() {
529 return d.ArgErr()
530 }
531 if s.FallbackRaw != nil {
532 return d.Err("fallback selection policy already specified")
533 }
534 mod, err := loadFallbackPolicy(d)
535 if err != nil {
536 return err
537 }
538 s.FallbackRaw = mod
539 default:
540 return d.Errf("unrecognized option '%s'", d.Val())
541 }
542 }
543 return nil
544 }
545
546 // HeaderHashSelection is a policy that selects
547 // a host based on a given request header.
548 type HeaderHashSelection struct {
549 // The HTTP header field whose value is to be hashed and used for upstream selection.
550 Field string `json:"field,omitempty"`
551
552 // The fallback policy to use if the header is not present. Defaults to `random`.
553 FallbackRaw json.RawMessage `json:"fallback,omitempty" caddy:"namespace=http.reverse_proxy.selection_policies inline_key=policy"`
554 fallback Selector
555 }
556
557 // CaddyModule returns the Caddy module information.
558 func (HeaderHashSelection) CaddyModule() caddy.ModuleInfo {
559 return caddy.ModuleInfo{
560 ID: "http.reverse_proxy.selection_policies.header",
561 New: func() caddy.Module { return new(HeaderHashSelection) },
562 }
563 }
564
565 // Provision sets up the module.
566 func (s *HeaderHashSelection) Provision(ctx caddy.Context) error {
567 if s.Field == "" {
568 return fmt.Errorf("header field is required")
569 }
570 if s.FallbackRaw == nil {
571 s.FallbackRaw = caddyconfig.JSONModuleObject(RandomSelection{}, "policy", "random", nil)
572 }
573 mod, err := ctx.LoadModule(s, "FallbackRaw")
574 if err != nil {
575 return fmt.Errorf("loading fallback selection policy: %s", err)
576 }
577 s.fallback = mod.(Selector)
578 return nil
579 }
580
581 // Select returns an available host, if any.
582 func (s HeaderHashSelection) Select(pool UpstreamPool, req *http.Request, _ http.ResponseWriter) *Upstream {
583 // The Host header should be obtained from the req.Host field
584 // since net/http removes it from the header map.
585 if s.Field == "Host" && req.Host != "" {
586 return hostByHashing(pool, req.Host)
587 }
588
589 val := req.Header.Get(s.Field)
590 if val == "" {
591 return s.fallback.Select(pool, req, nil)
592 }
593 return hostByHashing(pool, val)
594 }
595
596 // UnmarshalCaddyfile sets up the module from Caddyfile tokens.
597 func (s *HeaderHashSelection) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
598 d.Next() // consume policy name
599
600 if !d.NextArg() {
601 return d.ArgErr()
602 }
603 s.Field = d.Val()
604
605 for d.NextBlock(0) {
606 switch d.Val() {
607 case "fallback":
608 if !d.NextArg() {
609 return d.ArgErr()
610 }
611 if s.FallbackRaw != nil {
612 return d.Err("fallback selection policy already specified")
613 }
614 mod, err := loadFallbackPolicy(d)
615 if err != nil {
616 return err
617 }
618 s.FallbackRaw = mod
619 default:
620 return d.Errf("unrecognized option '%s'", d.Val())
621 }
622 }
623 return nil
624 }
625
626 // CookieHashSelection is a policy that selects
627 // a host based on a given cookie name.
628 type CookieHashSelection struct {
629 // The HTTP cookie name whose value is to be hashed and used for upstream selection.
630 Name string `json:"name,omitempty"`
631 // Secret to hash (Hmac256) chosen upstream in cookie
632 Secret string `json:"secret,omitempty"` //nolint:gosec // yes it's exported because it needs to encode to JSON
633 // The cookie's Max-Age before it expires. Default is no expiry.
634 MaxAge caddy.Duration `json:"max_age,omitempty"`
635
636 // The fallback policy to use if the cookie is not present. Defaults to `random`.
637 FallbackRaw json.RawMessage `json:"fallback,omitempty" caddy:"namespace=http.reverse_proxy.selection_policies inline_key=policy"`
638 fallback Selector
639 }
640
641 // CaddyModule returns the Caddy module information.
642 func (CookieHashSelection) CaddyModule() caddy.ModuleInfo {
643 return caddy.ModuleInfo{
644 ID: "http.reverse_proxy.selection_policies.cookie",
645 New: func() caddy.Module { return new(CookieHashSelection) },
646 }
647 }
648
649 // Provision sets up the module.
650 func (s *CookieHashSelection) Provision(ctx caddy.Context) error {
651 if s.Name == "" {
652 s.Name = "lb"
653 }
654 if s.FallbackRaw == nil {
655 s.FallbackRaw = caddyconfig.JSONModuleObject(RandomSelection{}, "policy", "random", nil)
656 }
657 mod, err := ctx.LoadModule(s, "FallbackRaw")
658 if err != nil {
659 return fmt.Errorf("loading fallback selection policy: %s", err)
660 }
661 s.fallback = mod.(Selector)
662 return nil
663 }
664
665 // Select returns an available host, if any.
666 func (s CookieHashSelection) Select(pool UpstreamPool, req *http.Request, w http.ResponseWriter) *Upstream {
667 // selects a new Host using the fallback policy (typically random)
668 // and write a sticky session cookie to the response.
669 selectNewHost := func() *Upstream {
670 upstream := s.fallback.Select(pool, req, w)
671 if upstream == nil {
672 return nil
673 }
674 sha, err := hashCookie(s.Secret, upstream.Dial)
675 if err != nil {
676 return upstream
677 }
678 cookie := &http.Cookie{
679 Name: s.Name,
680 Value: sha,
681 Path: "/",
682 Secure: false,
683 HttpOnly: true,
684 SameSite: http.SameSiteLaxMode,
685 }
686 isProxyHttps := false
687 if trusted, ok := caddyhttp.GetVar(req.Context(), caddyhttp.TrustedProxyVarKey).(bool); ok && trusted {
688 xfp, xfpOk, _ := lastHeaderValue(req.Header, "X-Forwarded-Proto")
689 isProxyHttps = xfpOk && xfp == "https"
690 }
691 if req.TLS != nil || isProxyHttps {
692 cookie.Secure = true
693 cookie.SameSite = http.SameSiteNoneMode
694 }
695 if s.MaxAge > 0 {
696 cookie.MaxAge = int(time.Duration(s.MaxAge).Seconds())
697 }
698 http.SetCookie(w, cookie)
699 return upstream
700 }
701
702 cookie, err := req.Cookie(s.Name)
703 // If there's no cookie, select a host using the fallback policy
704 if err != nil || cookie == nil {
705 return selectNewHost()
706 }
707 // If the cookie is present, loop over the available upstreams until we find a match
708 cookieValue := cookie.Value
709 for _, upstream := range pool {
710 if !upstream.Available() {
711 continue
712 }
713 sha, err := hashCookie(s.Secret, upstream.Dial)
714 if err == nil && hmac.Equal([]byte(sha), []byte(cookieValue)) {
715 return upstream
716 }
717 }
718 // If there is no matching host, select a host using the fallback policy
719 return selectNewHost()
720 }
721
722 // UnmarshalCaddyfile sets up the module from Caddyfile tokens. Syntax:
723 //
724 // lb_policy cookie [<name> [<secret>]] {
725 // fallback <policy>
726 // max_age <duration>
727 // }
728 //
729 // By default name is `lb`
730 func (s *CookieHashSelection) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
731 args := d.RemainingArgs()
732 switch len(args) {
733 case 1:
734 case 2:
735 s.Name = args[1]
736 case 3:
737 s.Name = args[1]
738 s.Secret = args[2]
739 default:
740 return d.ArgErr()
741 }
742 for d.NextBlock(0) {
743 switch d.Val() {
744 case "fallback":
745 if !d.NextArg() {
746 return d.ArgErr()
747 }
748 if s.FallbackRaw != nil {
749 return d.Err("fallback selection policy already specified")
750 }
751 mod, err := loadFallbackPolicy(d)
752 if err != nil {
753 return err
754 }
755 s.FallbackRaw = mod
756 case "max_age":
757 if !d.NextArg() {
758 return d.ArgErr()
759 }
760 if s.MaxAge != 0 {
761 return d.Err("cookie max_age already specified")
762 }
763 maxAge, err := caddy.ParseDuration(d.Val())
764 if err != nil {
765 return d.Errf("invalid duration: %s", d.Val())
766 }
767 if maxAge <= 0 {
768 return d.Errf("invalid duration: %s, max_age should be non-zero and positive", d.Val())
769 }
770 if d.NextArg() {
771 return d.ArgErr()
772 }
773 s.MaxAge = caddy.Duration(maxAge)
774 default:
775 return d.Errf("unrecognized option '%s'", d.Val())
776 }
777 }
778 return nil
779 }
780
781 // hashCookie hashes (HMAC 256) some data with the secret
782 func hashCookie(secret string, data string) (string, error) {
783 h := hmac.New(sha256.New, []byte(secret))
784 _, err := h.Write([]byte(data))
785 if err != nil {
786 return "", err
787 }
788 return hex.EncodeToString(h.Sum(nil)), nil
789 }
790
791 // selectRandomHost returns a random available host
792 func selectRandomHost(pool []*Upstream) *Upstream {
793 // use reservoir sampling because the number of available
794 // hosts isn't known: https://en.wikipedia.org/wiki/Reservoir_sampling
795 var randomHost *Upstream
796 var count int
797 for _, upstream := range pool {
798 if !upstream.Available() {
799 continue
800 }
801 // (n % 1 == 0) holds for all n, therefore a
802 // upstream will always be chosen if there is at
803 // least one available
804 count++
805 if weakrand.IntN(count) == 0 { //nolint:gosec
806 randomHost = upstream
807 }
808 }
809 return randomHost
810 }
811
812 // leastRequests returns the host with the
813 // least number of active requests to it.
814 // If more than one host has the same
815 // least number of active requests, then
816 // one of those is chosen at random.
817 func leastRequests(upstreams []*Upstream) *Upstream {
818 if len(upstreams) == 0 {
819 return nil
820 }
821 var best []*Upstream
822 bestReqs := -1
823 for _, upstream := range upstreams {
824 if upstream == nil {
825 continue
826 }
827 reqs := upstream.NumRequests()
828 if reqs == 0 {
829 return upstream
830 }
831 if reqs < bestReqs || bestReqs == -1 {
832 bestReqs = reqs
833 best = []*Upstream{upstream}
834 } else if reqs == bestReqs {
835 best = append(best, upstream)
836 }
837 }
838 if len(best) == 0 {
839 return nil
840 }
841 if len(best) == 1 {
842 return best[0]
843 }
844 return best[weakrand.IntN(len(best))] //nolint:gosec
845 }
846
847 // hostByHashing returns an available host from pool based on a hashable string s.
848 func hostByHashing(pool []*Upstream, s string) *Upstream {
849 // Highest Random Weight (HRW, or "Rendezvous") hashing,
850 // guarantees stability when the list of upstreams changes;
851 // see https://medium.com/i0exception/rendezvous-hashing-8c00e2fb58b0,
852 // https://randorithms.com/2020/12/26/rendezvous-hashing.html,
853 // and https://en.wikipedia.org/wiki/Rendezvous_hashing.
854 var highestHash uint64
855 var upstream *Upstream
856 for _, up := range pool {
857 if !up.Available() {
858 continue
859 }
860 h := hash(up.String() + s) // important to hash key and server together
861 if h > highestHash {
862 highestHash = h
863 upstream = up
864 }
865 }
866 return upstream
867 }
868
869 // hash calculates a fast hash based on s.
870 func hash(s string) uint64 {
871 h := xxhash.New()
872 _, _ = h.Write([]byte(s))
873 return h.Sum64()
874 }
875
876 func loadFallbackPolicy(d *caddyfile.Dispenser) (json.RawMessage, error) {
877 name := d.Val()
878 modID := "http.reverse_proxy.selection_policies." + name
879 unm, err := caddyfile.UnmarshalModule(d, modID)
880 if err != nil {
881 return nil, err
882 }
883 sel, ok := unm.(Selector)
884 if !ok {
885 return nil, d.Errf("module %s (%T) is not a reverseproxy.Selector", modID, unm)
886 }
887 return caddyconfig.JSONModuleObject(sel, "policy", name, nil), nil
888 }
889
890 // Interface guards
891 var (
892 _ Selector = (*RandomSelection)(nil)
893 _ Selector = (*RandomChoiceSelection)(nil)
894 _ Selector = (*LeastConnSelection)(nil)
895 _ Selector = (*RoundRobinSelection)(nil)
896 _ Selector = (*WeightedRoundRobinSelection)(nil)
897 _ Selector = (*FirstSelection)(nil)
898 _ Selector = (*IPHashSelection)(nil)
899 _ Selector = (*ClientIPHashSelection)(nil)
900 _ Selector = (*URIHashSelection)(nil)
901 _ Selector = (*QueryHashSelection)(nil)
902 _ Selector = (*HeaderHashSelection)(nil)
903 _ Selector = (*CookieHashSelection)(nil)
904
905 _ caddy.Validator = (*RandomChoiceSelection)(nil)
906 _ caddy.Validator = (*WeightedRoundRobinSelection)(nil)
907
908 _ caddy.Provisioner = (*RandomChoiceSelection)(nil)
909 _ caddy.Provisioner = (*WeightedRoundRobinSelection)(nil)
910
911 _ caddyfile.Unmarshaler = (*RandomChoiceSelection)(nil)
912 _ caddyfile.Unmarshaler = (*WeightedRoundRobinSelection)(nil)
913 )