vouch-proxy/pkg/cookie/cookie.go

212 lines
5.9 KiB
Go

/*
Copyright 2020 The Vouch Proxy Authors.
Use of this source code is governed by The MIT License (MIT) that
can be found in the LICENSE file. Software distributed under The
MIT License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
OR CONDITIONS OF ANY KIND, either express or implied.
*/
package cookie
import (
"errors"
"fmt"
"net/http"
"strconv"
"strings"
"unicode/utf8"
// "github.com/vouch/vouch-proxy/pkg/structs"
"github.com/vouch/vouch-proxy/pkg/cfg"
"github.com/vouch/vouch-proxy/pkg/domains"
"go.uber.org/zap"
)
const maxCookieSize = 4000
var log *zap.SugaredLogger
var sameSite http.SameSite
// Configure see main.go configure()
func Configure() {
log = cfg.Logging.Logger
sameSite = SameSite()
}
// SetCookie http
func SetCookie(w http.ResponseWriter, r *http.Request, val string) {
setCookie(w, r, val, cfg.Cfg.Cookie.MaxAge*60) // convert minutes to seconds
}
func setCookie(w http.ResponseWriter, r *http.Request, val string, maxAge int) {
cookieName := cfg.Cfg.Cookie.Name
// foreach domain
domain := domains.Matches(r.Host)
// Allow overriding the cookie domain in the config file
if cfg.Cfg.Cookie.Domain != "" {
domain = cfg.Cfg.Cookie.Domain
log.Debugf("setting the cookie domain to %v", domain)
}
cookie := http.Cookie{
Name: cfg.Cfg.Cookie.Name,
Value: val,
Path: "/",
Domain: domain,
MaxAge: maxAge,
Secure: cfg.Cfg.Cookie.Secure,
HttpOnly: cfg.Cfg.Cookie.HTTPOnly,
SameSite: sameSite,
}
cookieSize := len(cookie.String())
cookie.Value = ""
emptyCookieSize := len(cookie.String())
// Cookies have a max size of 4096 bytes, but to support most browsers, we should stay below 4000 bytes
// https://tools.ietf.org/html/rfc6265#section-6.1
// http://browsercookielimits.squawky.net/
if cookieSize > maxCookieSize {
// https://www.lifewire.com/cookie-limit-per-domain-3466809
log.Warnf("cookie size: %d. cookie sizes over ~4093 bytes(depending on the browser and platform) have shown to cause issues or simply aren't supported.", cookieSize)
cookieParts := splitCookie(val, maxCookieSize-emptyCookieSize)
for i, cookiePart := range cookieParts {
// Cookies are named 1of3, 2of3, 3of3
cookieName = fmt.Sprintf("%s_%dof%d", cfg.Cfg.Cookie.Name, i+1, len(cookieParts))
http.SetCookie(w, &http.Cookie{
Name: cookieName,
Value: cookiePart,
Path: "/",
Domain: domain,
MaxAge: maxAge,
Secure: cfg.Cfg.Cookie.Secure,
HttpOnly: cfg.Cfg.Cookie.HTTPOnly,
SameSite: sameSite,
})
}
} else {
http.SetCookie(w, &http.Cookie{
Name: cookieName,
Value: val,
Path: "/",
Domain: domain,
MaxAge: maxAge,
Secure: cfg.Cfg.Cookie.Secure,
HttpOnly: cfg.Cfg.Cookie.HTTPOnly,
SameSite: sameSite,
})
}
}
// Cookie get the vouch jwt cookie
func Cookie(r *http.Request) (string, error) {
cookieParts := make([]string, 0)
var numParts = -1
var err error
cookies := r.Cookies()
// Get the remaining parts
// search for cookie parts in order
// this is the hotpath so we're trying to only walk once
for _, cookie := range cookies {
if cookie.Name == cfg.Cfg.Cookie.Name {
return cookie.Value, nil
}
cookieUnder := fmt.Sprintf("%s_", cfg.Cfg.Cookie.Name)
if strings.HasPrefix(cookie.Name, cookieUnder) {
log.Debugw("cookie",
"cookieName", cookie.Name,
"cookieValue", cookie.Value,
)
xOFy := strings.Replace(cookie.Name, cookieUnder, "", 1)
xyArray := strings.Split(xOFy, "of")
if numParts == -1 { // then its uninitialized
if numParts, err = strconv.Atoi(xyArray[1]); err != nil {
return "", fmt.Errorf("multipart cookie fail: %s", err)
}
log.Debugf("make cookieParts of size %d", numParts)
cookieParts = make([]string, numParts)
}
var i int
if i, err = strconv.Atoi(xyArray[0]); err != nil {
return "", fmt.Errorf("multipart cookie fail: %s", err)
}
cookieParts[i-1] = cookie.Value
}
}
// combinedCookieStr := combinedCookie.String()
combinedCookieStr := strings.Join(cookieParts, "")
if combinedCookieStr == "" {
return "", errors.New("cookie token empty")
}
log.Debugw("combined cookie",
"cookieValue", combinedCookieStr,
)
return combinedCookieStr, err
}
// ClearCookie get rid of the existing cookie
func ClearCookie(w http.ResponseWriter, r *http.Request) {
cookies := r.Cookies()
domain := domains.Matches(r.Host)
// Allow overriding the cookie domain in the config file
if cfg.Cfg.Cookie.Domain != "" {
domain = cfg.Cfg.Cookie.Domain
log.Debugf("setting the cookie domain to %v", domain)
}
// search for cookie parts
for _, cookie := range cookies {
if strings.HasPrefix(cookie.Name, cfg.Cfg.Cookie.Name) {
log.Debugf("deleting cookie: %s", cookie.Name)
http.SetCookie(w, &http.Cookie{
Name: cookie.Name,
Value: "delete",
Path: "/",
Domain: domain,
MaxAge: -1,
Secure: cfg.Cfg.Cookie.Secure,
HttpOnly: cfg.Cfg.Cookie.HTTPOnly,
})
}
}
}
// SameSite return cfg.Cfg.Cookie.SameSite as http.Samesite
// if cfg.Cfg.Cookie.SameSite is unconfigured return http.SameSite(0)
// see https://github.com/vouch/vouch-proxy/issues/210
func SameSite() http.SameSite {
sameSite := http.SameSite(0)
if cfg.Cfg.Cookie.SameSite != "" {
switch strings.ToLower(cfg.Cfg.Cookie.SameSite) {
case "lax":
sameSite = http.SameSiteLaxMode
case "strict":
sameSite = http.SameSiteStrictMode
case "none":
if cfg.Cfg.Cookie.Secure == false {
log.Error("SameSite cookie attribute with sameSite=none should also be specified with secure=true.")
}
sameSite = http.SameSiteNoneMode
}
}
return sameSite
}
// splitCookie separate string into several strings of specified length
func splitCookie(longString string, maxLen int) []string {
splits := make([]string, 0)
var l, r int
for l, r = 0, maxLen; r < len(longString); l, r = r, r+maxLen {
for !utf8.RuneStart(longString[r]) {
r--
}
splits = append(splits, longString[l:r])
}
splits = append(splits, longString[l:])
return splits
}