Skip to content
This repository has been archived by the owner on Jul 15, 2021. It is now read-only.

Commit

Permalink
Merge pull request #117 from Croissong/fix/split_large_cookies
Browse files Browse the repository at this point in the history
fix: Add custom sessionstore with cookie-splitting functionality
  • Loading branch information
alexbrand committed Sep 3, 2019
2 parents 0876183 + a1a811c commit ca1538e
Show file tree
Hide file tree
Showing 5 changed files with 295 additions and 11 deletions.
1 change: 1 addition & 0 deletions Gopkg.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 2 additions & 6 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ setup:
go get -u github.com/golang/dep/cmd/dep
go get -u github.com/mjibson/esc/...

check: test vet gofmt staticcheck unused misspell
check: test vet gofmt staticcheck misspell

deps:
dep ensure -v
Expand All @@ -49,11 +49,7 @@ test:

staticcheck:
@go get honnef.co/go/tools/cmd/staticcheck
staticcheck $(PKGS)

unused:
@go get honnef.co/go/tools/cmd/unused
unused -exported $(PKGS)
staticcheck -unused.whole-program $(PKGS)

misspell:
@go get github.com/client9/misspell/cmd/misspell
Expand Down
8 changes: 3 additions & 5 deletions internal/session/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,21 @@ package session

import (
"crypto/sha256"
"net/http"

"github.com/gorilla/sessions"
"golang.org/x/crypto/pbkdf2"
"net/http"
)

const salt = "MkmfuPNHnZBBivy0L0aW"

// Session defines a Gangway session
type Session struct {
Session *sessions.CookieStore
Session *CustomCookieStore
}

// New inits a Session with CookieStore
func New(sessionSecurityKey string) *Session {
return &Session{
Session: sessions.NewCookieStore(generateSessionKeys(sessionSecurityKey)),
Session: NewCustomCookieStore(generateSessionKeys(sessionSecurityKey)),
}
}

Expand Down
135 changes: 135 additions & 0 deletions internal/session/store.go
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)
}
154 changes: 154 additions & 0 deletions internal/session/store_test.go
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)
}

0 comments on commit ca1538e

Please sign in to comment.