Skip to content

Commit d8afb49

Browse files
committed
support anonymous user with sticky cookie
1 parent 73cec76 commit d8afb49

File tree

5 files changed

+118
-8
lines changed

5 files changed

+118
-8
lines changed

pkg/config/dynamic/http_config.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ type WRRService struct {
9797
// +k8s:deepcopy-gen=true
9898

9999
// LabeledRoundRobin defines a labeled load-balancer of services, which select service by label.
100-
// Label will be extract from request header or cookie, with key `X-Canary-Label`.
100+
// Label will be extract from request header or cookie, with key `X-Canary`.
101101
// services should be named as `{defaultService}-{label}`. Ex. "myservice-stable", "myservice-beta", "myservice-dev"
102102
type LabeledRoundRobin struct {
103103
ServiceName string `json:"serviceName,omitempty" toml:"serviceName,omitempty" yaml:"serviceName,omitempty"`

pkg/config/dynamic/middlewares.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -516,4 +516,5 @@ type Canary struct {
516516
MaxCacheSize int `json:"maxCacheSize,omitempty" toml:"maxCacheSize,omitempty" yaml:"maxCacheSize,omitempty" export:"true"`
517517
CacheExpiration types.Duration `json:"cacheExpiration,omitempty" toml:"cacheExpiration,omitempty" yaml:"cacheExpiration,omitempty" export:"true"`
518518
CacheCleanDuration types.Duration `json:"cacheCleanDuration,omitempty" toml:"cacheCleanDuration,omitempty" yaml:"cacheCleanDuration,omitempty" export:"true"`
519+
Sticky *Sticky `json:"sticky,omitempty" toml:"sticky,omitempty" yaml:"sticky,omitempty" export:"true"`
519520
}

pkg/middlewares/canary/canary.go

Lines changed: 79 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@ package canary
22

33
import (
44
"context"
5+
"crypto/sha1"
56
"encoding/base64"
67
"encoding/json"
78
"fmt"
9+
"io"
10+
"net"
811
"net/http"
912
"regexp"
1013
"strings"
@@ -14,6 +17,7 @@ import (
1417
"github.com/containous/traefik/v2/pkg/log"
1518
"github.com/containous/traefik/v2/pkg/middlewares"
1619
"github.com/containous/traefik/v2/pkg/middlewares/accesslog"
20+
"github.com/containous/traefik/v2/pkg/server/cookie"
1721
"github.com/opentracing/opentracing-go"
1822
"github.com/opentracing/opentracing-go/ext"
1923
)
@@ -42,6 +46,7 @@ type Canary struct {
4246
canaryResponseHeader bool
4347
loadLabels bool
4448
ls *LabelStore
49+
sticky *dynamic.Sticky
4550
next http.Handler
4651
}
4752

@@ -74,7 +79,16 @@ func New(ctx context.Context, next http.Handler, cfg dynamic.Canary, name string
7479
loadLabels: cfg.Server != "",
7580
addRequestID: cfg.AddRequestID,
7681
canaryResponseHeader: cfg.CanaryResponseHeader,
82+
sticky: cfg.Sticky,
7783
}
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+
7892
if c.loadLabels {
7993
c.ls = NewLabelStore(logger, cfg, expiration, cacheCleanDuration)
8094
}
@@ -142,6 +156,18 @@ func (c *Canary) processCanary(rw http.ResponseWriter, req *http.Request) {
142156
info.product = c.product
143157
info.uid = extractUserID(req, c.uidCookies)
144158

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+
145171
if info.label == "" && info.uid != "" {
146172
labels := c.ls.MustLoadLabels(req.Context(), info.uid, req.Header.Get(headerXRequestID))
147173
for _, l := range labels {
@@ -167,13 +193,27 @@ func (c *Canary) processCanary(rw http.ResponseWriter, req *http.Request) {
167193
}
168194
}
169195

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+
170210
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"`
177217
}
178218

179219
func extractUserID(req *http.Request, uidCookies []string) string {
@@ -323,6 +363,9 @@ func (ch *canaryHeader) feed(vals []string, trust bool) {
323363
}
324364
}
325365
}
366+
if ch.testing && ch.label == "" {
367+
ch.label = "testing"
368+
}
326369
}
327370

328371
// label should not be empty
@@ -358,3 +401,33 @@ func (ch *canaryHeader) String() string {
358401
}
359402
return strings.Join(vals, ",")
360403
}
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+
}

pkg/middlewares/canary/canary_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,4 +302,40 @@ func TestCanary(t *testing.T) {
302302
a.Equal("iOS", ch.client)
303303
a.Equal("someuid", ch.uid)
304304
})
305+
306+
t.Run("sticky should work", func(t *testing.T) {
307+
a := assert.New(t)
308+
309+
cfg := dynamic.Canary{MaxCacheSize: 3, Server: "localhost", Product: "Urbs", Sticky: &dynamic.Sticky{
310+
Cookie: &dynamic.Cookie{Name: "_urbs_"},
311+
}}
312+
c, err := New(context.Background(), next, cfg, "test")
313+
c.ls.mustFetchLabels = func(ctx context.Context, uid, requestID string) ([]Label, int64) {
314+
return []Label{{Label: uid}}, time.Now().Unix()
315+
}
316+
a.Nil(err)
317+
318+
req := httptest.NewRequest("GET", "http://example.com/foo", nil)
319+
rw := httptest.NewRecorder()
320+
c.processCanary(rw, req)
321+
ch := &canaryHeader{}
322+
ch.fromHeader(req.Header, true)
323+
a.NotEqual("", ch.label)
324+
a.Equal(ch.uid, ch.label)
325+
a.Contains(rw.Header().Get("Set-Cookie"), "_urbs_=ey")
326+
327+
uid := ch.uid
328+
cookies := rw.Result().Cookies()
329+
a.Equal(1, len(cookies))
330+
a.Equal("_urbs_", cookies[0].Name)
331+
332+
req = httptest.NewRequest("GET", "http://example.com/foo", nil)
333+
req.AddCookie(cookies[0])
334+
rw = httptest.NewRecorder()
335+
c.processCanary(rw, req)
336+
ch = &canaryHeader{}
337+
ch.fromHeader(req.Header, true)
338+
a.Equal(uid, ch.label)
339+
a.Equal(ch.uid, ch.label)
340+
})
305341
}

pkg/provider/kubernetes/crd/traefik/v1alpha1/service.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ type WeightedRoundRobin struct {
6969
// +k8s:deepcopy-gen=true
7070

7171
// LabeledRoundRobin defines a labeled load-balancer of services, which select service by label.
72-
// Label will be extract from request header or cookie, with key `X-Canary-Label`.
72+
// Label will be extract from request header or cookie, with key `X-Canary`.
7373
// services should be named as `{defaultService}-{label}`. Ex. "myservice-stable", "myservice-beta", "myservice-dev"
7474
type LabeledRoundRobin struct {
7575
Service

0 commit comments

Comments
 (0)