This repository has been archived by the owner on Jul 15, 2021. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 112
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #117 from Croissong/fix/split_large_cookies
fix: Add custom sessionstore with cookie-splitting functionality
- Loading branch information
Showing
5 changed files
with
295 additions
and
11 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
// Copyright © 2019 Heptio | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
package session | ||
|
||
import ( | ||
"fmt" | ||
"github.com/gorilla/securecookie" | ||
"github.com/gorilla/sessions" | ||
"net/http" | ||
) | ||
|
||
// The CustomCookieStore automatically splits cookies with length greater than maxCookieLength into multiple smaller cookies. | ||
// The motivation is the browsers' 4KB limit on cookies, which for instance causes problems for large id_tokens in azure. | ||
|
||
const ( | ||
// Cookies are limited to 4kb including the length of the cookie name, | ||
// the cookie name can be up to 256 bytes | ||
maxCookieLength = 3840 | ||
) | ||
|
||
type CustomCookieStore struct { | ||
*sessions.CookieStore | ||
} | ||
|
||
// Set secureCookie maxLength to an arbitrary (20x4kb) high value since we are no longer limited | ||
func NewCustomCookieStore(keyPairs ...[]byte) *CustomCookieStore { | ||
cookieStore := sessions.NewCookieStore(keyPairs...) | ||
for _, codec := range cookieStore.Codecs { | ||
cookie := codec.(*securecookie.SecureCookie) | ||
cookie.MaxLength(81920) | ||
} | ||
return &CustomCookieStore{cookieStore} | ||
} | ||
|
||
func (s *CustomCookieStore) Get(r *http.Request, name string) (*sessions.Session, error) { | ||
return sessions.GetRegistry(r).Get(s, name) | ||
} | ||
|
||
// In contrast to default implementation, the session values can be partitioned into | ||
// multiple cookies. | ||
// The original cookie is split/joined in its encoded form | ||
func (s *CustomCookieStore) New(r *http.Request, name string) (*sessions.Session, error) { | ||
session := sessions.NewSession(s, name) | ||
opts := *s.Options | ||
session.Options = &opts | ||
session.IsNew = true | ||
cookie := joinSectionCookies(r, name) | ||
var err error | ||
if len(cookie) > 0 { | ||
err = securecookie.DecodeMulti(name, cookie, &session.Values, s.Codecs...) | ||
if err == nil { | ||
session.IsNew = false | ||
} | ||
} | ||
return session, err | ||
} | ||
|
||
// If the cookie length is > maxCookieLength, its value is split into multiple cookies | ||
// fitting into the maxCookieLength limit. | ||
// The resulting section cookies get their index appended to the name. | ||
func (s *CustomCookieStore) Save(r *http.Request, w http.ResponseWriter, | ||
session *sessions.Session) error { | ||
|
||
cookie, err := securecookie.EncodeMulti(session.Name(), session.Values, | ||
s.Codecs...) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
sectionCookies := splitCookie(cookie) | ||
// With a singular section the name is unchanged | ||
if len(sectionCookies) == 1 { | ||
cookieName := session.Name() | ||
http.SetCookie(w, sessions.NewCookie(cookieName, sectionCookies[0], session.Options)) | ||
return nil | ||
} | ||
|
||
for i, value := range sectionCookies { | ||
cookieName := buildSectionCookieName(session.Name(), i) | ||
http.SetCookie(w, sessions.NewCookie(cookieName, value, session.Options)) | ||
} | ||
return nil | ||
} | ||
|
||
// joinCookies concatenates the values of all matching cookies and returns the original, encoded cookievalue string. | ||
func joinSectionCookies(r *http.Request, name string) string { | ||
|
||
// Exact match without index means only a single cookie exists | ||
if c, err := r.Cookie(name); err == nil { | ||
return c.Value | ||
} | ||
|
||
var joinedValue string | ||
for i := 0; true; i++ { | ||
cookieName := buildSectionCookieName(name, i) | ||
if c, err := r.Cookie(cookieName); err == nil { | ||
joinedValue += c.Value | ||
} else { | ||
break | ||
} | ||
} | ||
return joinedValue | ||
} | ||
|
||
// splitCookie splits the original encoded cookie value into a slice of cookies which | ||
// fit within the 4kb cookie limit indexing the cookies from 0 | ||
func splitCookie(cookieValue string) []string { | ||
var sectionCookies []string | ||
valueBytes := []byte(cookieValue) | ||
|
||
for len(valueBytes) > 0 { | ||
length := len(valueBytes) | ||
if length > maxCookieLength { | ||
length = maxCookieLength | ||
} | ||
sectionCookies = append(sectionCookies, string(valueBytes[:length])) | ||
valueBytes = valueBytes[length:] | ||
} | ||
return sectionCookies | ||
} | ||
|
||
func buildSectionCookieName(name string, index int) string { | ||
return fmt.Sprintf("%s_%d", name, index) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,154 @@ | ||
// Copyright © 2019 Heptio | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
package session | ||
|
||
import ( | ||
"fmt" | ||
"github.com/gorilla/sessions" | ||
log "github.com/sirupsen/logrus" | ||
"math" | ||
"math/rand" | ||
"net/http" | ||
"net/http/httptest" | ||
"strings" | ||
"testing" | ||
) | ||
|
||
func TestJoinSectionCookies(t *testing.T) { | ||
var originalValue string | ||
var value string | ||
cookies := buildRandomCookies(2, 3800, "test_%d") | ||
buildRequestWithCookies(cookies, func(cookies []*http.Cookie, r *http.Request) { | ||
for _, c := range cookies { | ||
originalValue += c.Value | ||
} | ||
value = joinSectionCookies(r, "test") | ||
}) | ||
if value != originalValue { | ||
t.Errorf("joinSectionCookies value incorrect: \n value: %s \n originalValue: %s", value, originalValue) | ||
} | ||
} | ||
|
||
func TestJoinSectionCookiesSingle(t *testing.T) { | ||
var originalValue string | ||
var value string | ||
cookies := buildRandomCookies(1, 2000, "test_%d") | ||
buildRequestWithCookies(cookies, func(cookies []*http.Cookie, r *http.Request) { | ||
for _, c := range cookies { | ||
originalValue += c.Value | ||
} | ||
value = joinSectionCookies(r, "test") | ||
}) | ||
if value != originalValue { | ||
t.Errorf("joinSectionCookies value incorrect: \n value: %s \n originalValue: %s", value, originalValue) | ||
} | ||
} | ||
|
||
func TestSplitCookie(t *testing.T) { | ||
cookieLength := 8000 | ||
originalValue := randStringBytesRmndr(cookieLength) | ||
sectionCookies := splitCookie(originalValue) | ||
expectedCount := int(math.Ceil((float64(cookieLength) / maxCookieLength))) | ||
if len(sectionCookies) != expectedCount { | ||
t.Errorf("splitCookie count incorrect: \n count: %d \n expectedCount: %d", len(sectionCookies), expectedCount) | ||
} | ||
value := strings.Join(sectionCookies, "") | ||
if value != originalValue { | ||
t.Errorf("splitCookie value incorrect: \n value: %s \n originalValue: %s", value, originalValue) | ||
} | ||
} | ||
|
||
func TestSplitCookieSingle(t *testing.T) { | ||
cookieLength := 2000 | ||
originalValue := randStringBytesRmndr(cookieLength) | ||
sectionCookies := splitCookie(originalValue) | ||
expectedCount := int(math.Ceil((float64(cookieLength) / maxCookieLength))) | ||
if len(sectionCookies) != expectedCount { | ||
t.Errorf("splitCookie count incorrect: \n count: %d \n expectedCount: %d", len(sectionCookies), expectedCount) | ||
} | ||
} | ||
|
||
func TestSplitCookieSize(t *testing.T) { | ||
cookieLength := 10000 | ||
originalValue := randStringBytesRmndr(cookieLength) | ||
sectionCookies := splitCookie(originalValue) | ||
for _, s := range sectionCookies { | ||
if len(s) > maxCookieLength { | ||
t.Errorf("sectionCookie length over limit: \n length: %d", len(s)) | ||
} | ||
} | ||
} | ||
|
||
func TestSplitAndJoin(t *testing.T) { | ||
cookieLength := 10000 | ||
originalValue := randStringBytesRmndr(cookieLength) | ||
sectionCookies := splitCookie(originalValue) | ||
cookies := buildCookiesFromValues(sectionCookies, "test_%d") | ||
var value string | ||
buildRequestWithCookies(cookies, func(cookies []*http.Cookie, r *http.Request) { | ||
value = joinSectionCookies(r, "test") | ||
}) | ||
if value != originalValue { | ||
t.Errorf("SplitAndJoin value incorrect: \n value: %s \n originalValue: %s", value, originalValue) | ||
} | ||
} | ||
|
||
// Utility | ||
|
||
type handleReq func([]*http.Cookie, *http.Request) | ||
|
||
func buildRequestWithCookies(cookies []*http.Cookie, fn handleReq) { | ||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
for _, cookie := range cookies { | ||
r.AddCookie(cookie) | ||
} | ||
fn(cookies, r) | ||
})) | ||
defer ts.Close() | ||
_, err := http.Get(ts.URL) | ||
if err != nil { | ||
log.Fatal(err) | ||
} | ||
} | ||
|
||
func buildRandomCookies(cookieCount int, cookieLength int, cookieName string) []*http.Cookie { | ||
sessionOptions := &sessions.Options{} | ||
var cookies []*http.Cookie | ||
for i := 0; i < cookieCount; i++ { | ||
value := randStringBytesRmndr(cookieLength) | ||
cookie := sessions.NewCookie(fmt.Sprintf(cookieName, i), value, sessionOptions) | ||
cookies = append(cookies, cookie) | ||
} | ||
return cookies | ||
} | ||
|
||
func buildCookiesFromValues(values []string, cookieName string) []*http.Cookie { | ||
sessionOptions := &sessions.Options{} | ||
var cookies []*http.Cookie | ||
for i, value := range values { | ||
cookie := sessions.NewCookie(fmt.Sprintf(cookieName, i), value, sessionOptions) | ||
cookies = append(cookies, cookie) | ||
} | ||
return cookies | ||
} | ||
|
||
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" | ||
|
||
func randStringBytesRmndr(n int) string { | ||
b := make([]byte, n) | ||
for i := range b { | ||
b[i] = letterBytes[rand.Int63()%int64(len(letterBytes))] | ||
} | ||
return string(b) | ||
} |