@@ -2,9 +2,12 @@ package canary
2
2
3
3
import (
4
4
"context"
5
+ "crypto/sha1"
5
6
"encoding/base64"
6
7
"encoding/json"
7
8
"fmt"
9
+ "io"
10
+ "net"
8
11
"net/http"
9
12
"regexp"
10
13
"strings"
@@ -14,6 +17,7 @@ import (
14
17
"github.com/containous/traefik/v2/pkg/log"
15
18
"github.com/containous/traefik/v2/pkg/middlewares"
16
19
"github.com/containous/traefik/v2/pkg/middlewares/accesslog"
20
+ "github.com/containous/traefik/v2/pkg/server/cookie"
17
21
"github.com/opentracing/opentracing-go"
18
22
"github.com/opentracing/opentracing-go/ext"
19
23
)
@@ -42,6 +46,7 @@ type Canary struct {
42
46
canaryResponseHeader bool
43
47
loadLabels bool
44
48
ls * LabelStore
49
+ sticky * dynamic.Sticky
45
50
next http.Handler
46
51
}
47
52
@@ -74,7 +79,16 @@ func New(ctx context.Context, next http.Handler, cfg dynamic.Canary, name string
74
79
loadLabels : cfg .Server != "" ,
75
80
addRequestID : cfg .AddRequestID ,
76
81
canaryResponseHeader : cfg .CanaryResponseHeader ,
82
+ sticky : cfg .Sticky ,
77
83
}
84
+
85
+ if cfg .Sticky != nil {
86
+ c .sticky .Cookie .Name = cookie .GetName (cfg .Sticky .Cookie .Name , name )
87
+ if ! strSliceHas (c .uidCookies , c .sticky .Cookie .Name ) {
88
+ c .uidCookies = append (c .uidCookies , c .sticky .Cookie .Name )
89
+ }
90
+ }
91
+
78
92
if c .loadLabels {
79
93
c .ls = NewLabelStore (logger , cfg , expiration , cacheCleanDuration )
80
94
}
@@ -142,6 +156,18 @@ func (c *Canary) processCanary(rw http.ResponseWriter, req *http.Request) {
142
156
info .product = c .product
143
157
info .uid = extractUserID (req , c .uidCookies )
144
158
159
+ if info .uid == "" && c .sticky != nil {
160
+ addr := req .Header .Get ("X-Real-Ip" )
161
+ if addr == "" {
162
+ addr = req .Header .Get ("X-Forwarded-For" )
163
+ }
164
+ if addr == "" {
165
+ addr , _ , _ = net .SplitHostPort (req .RemoteAddr )
166
+ }
167
+ info .uid = anonymousID (addr , req .Header .Get (headerUA ), req .Header .Get ("Cookie" ), time .Now ().Format (time .RFC822 ))
168
+ c .addSticky (info .uid , rw )
169
+ }
170
+
145
171
if info .label == "" && info .uid != "" {
146
172
labels := c .ls .MustLoadLabels (req .Context (), info .uid , req .Header .Get (headerXRequestID ))
147
173
for _ , l := range labels {
@@ -167,13 +193,27 @@ func (c *Canary) processCanary(rw http.ResponseWriter, req *http.Request) {
167
193
}
168
194
}
169
195
196
+ func (c * Canary ) addSticky (id string , rw http.ResponseWriter ) {
197
+ if data , err := json .Marshal (userInfo {UID5 : id }); err == nil {
198
+ http .SetCookie (rw , & http.Cookie {
199
+ Name : c .sticky .Cookie .Name ,
200
+ Value : base64 .RawURLEncoding .EncodeToString (data ),
201
+ Path : "/" ,
202
+ MaxAge : 60 * 60 * 24 * 7 ,
203
+ Secure : c .sticky .Cookie .Secure ,
204
+ HttpOnly : c .sticky .Cookie .HTTPOnly ,
205
+ SameSite : convertSameSite (c .sticky .Cookie .SameSite ),
206
+ })
207
+ }
208
+ }
209
+
170
210
type userInfo struct {
171
- UID0 string `json:"uid"`
172
- UID1 string `json:"_userId"`
173
- UID2 string `json:"userId"`
174
- UID3 string `json:"user_id"`
175
- UID4 string `json:"sub"`
176
- UID5 string `json:"id"`
211
+ UID0 string `json:"uid,omitempty "`
212
+ UID1 string `json:"_userId,omitempty "`
213
+ UID2 string `json:"userId,omitempty "`
214
+ UID3 string `json:"user_id,omitempty "`
215
+ UID4 string `json:"sub,omitempty "`
216
+ UID5 string `json:"id,omitempty "`
177
217
}
178
218
179
219
func extractUserID (req * http.Request , uidCookies []string ) string {
@@ -323,6 +363,9 @@ func (ch *canaryHeader) feed(vals []string, trust bool) {
323
363
}
324
364
}
325
365
}
366
+ if ch .testing && ch .label == "" {
367
+ ch .label = "testing"
368
+ }
326
369
}
327
370
328
371
// label should not be empty
@@ -358,3 +401,33 @@ func (ch *canaryHeader) String() string {
358
401
}
359
402
return strings .Join (vals , "," )
360
403
}
404
+
405
+ func convertSameSite (sameSite string ) http.SameSite {
406
+ switch sameSite {
407
+ case "none" :
408
+ return http .SameSiteNoneMode
409
+ case "lax" :
410
+ return http .SameSiteLaxMode
411
+ case "strict" :
412
+ return http .SameSiteStrictMode
413
+ default :
414
+ return 0
415
+ }
416
+ }
417
+
418
+ func anonymousID (feeds ... string ) string {
419
+ h := sha1 .New ()
420
+ for _ , v := range feeds {
421
+ io .WriteString (h , v )
422
+ }
423
+ return fmt .Sprintf ("anon-%x" , h .Sum (nil ))
424
+ }
425
+
426
+ func strSliceHas (s []string , t string ) bool {
427
+ for _ , v := range s {
428
+ if v == t {
429
+ return true
430
+ }
431
+ }
432
+ return false
433
+ }
0 commit comments