mirror of https://github.com/vouch/vouch-proxy
212 lines
5.9 KiB
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
|
|
}
|