io.undertow.util.Cookies Maven / Gradle / Ivy
/*
* JBoss, Home of Professional Open Source.
* Copyright 2014 Red Hat, Inc., and individual contributors
* as indicated by the @author tags.
*
* 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 io.undertow.util;
import io.undertow.UndertowLogger;
import io.undertow.UndertowMessages;
import io.undertow.server.handlers.Cookie;
import io.undertow.server.handlers.CookieImpl;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
/**
* Class that contains utility methods for dealing with cookies.
*
* @author Stuart Douglas
* @author Andre Dietisheim
* @author Richard Opalka
*/
public class Cookies {
public static final String DOMAIN = "$Domain";
public static final String VERSION = "$Version";
public static final String PATH = "$Path";
/**
* Parses a "Set-Cookie:" response header value into its cookie representation. The header value is parsed according to the
* syntax that's defined in RFC2109:
*
*
*
* set-cookie = "Set-Cookie:" cookies
* cookies = 1#cookie
* cookie = NAME "=" VALUE *(";" cookie-av)
* NAME = attr
* VALUE = value
* cookie-av = "Comment" "=" value
* | "Domain" "=" value
* | "Max-Age" "=" value
* | "Path" "=" value
* | "Secure"
* | "Version" "=" 1*DIGIT
*
*
*
*
* @param headerValue The header value
* @return The cookie
*
* @see Cookie
* @see rfc2109
*/
public static Cookie parseSetCookieHeader(final String headerValue) {
String key = null;
CookieImpl cookie = null;
int state = 0;
int current = 0;
for (int i = 0; i < headerValue.length(); ++i) {
char c = headerValue.charAt(i);
switch (state) {
case 0: {
//reading key
if (c == '=') {
key = headerValue.substring(current, i);
current = i + 1;
state = 1;
} else if ((c == ';' || c == ' ') && current == i) {
current++;
} else if (c == ';') {
if (cookie == null) {
throw UndertowMessages.MESSAGES.couldNotParseCookie(headerValue);
} else {
handleValue(cookie, headerValue.substring(current, i), null);
}
current = i + 1;
}
break;
}
case 1: {
if (c == ';') {
if (cookie == null) {
cookie = new CookieImpl(key, headerValue.substring(current, i));
} else {
handleValue(cookie, key, headerValue.substring(current, i));
}
state = 0;
current = i + 1;
key = null;
} else if (c == '"' && current == i) {
current++;
state = 2;
}
break;
}
case 2: {
if (c == '"') {
if (cookie == null) {
cookie = new CookieImpl(key, headerValue.substring(current, i));
} else {
handleValue(cookie, key, headerValue.substring(current, i));
}
state = 0;
current = i + 1;
key = null;
}
break;
}
}
}
if (key == null) {
if (current != headerValue.length()) {
handleValue(cookie, headerValue.substring(current, headerValue.length()), null);
}
} else {
if (current != headerValue.length()) {
if(cookie == null) {
cookie = new CookieImpl(key, headerValue.substring(current, headerValue.length()));
} else {
handleValue(cookie, key, headerValue.substring(current, headerValue.length()));
}
} else {
handleValue(cookie, key, null);
}
}
return cookie;
}
private static void handleValue(CookieImpl cookie, String key, String value) {
if (key == null) {
return;
}
if (key.equalsIgnoreCase("path")) {
cookie.setPath(value);
} else if (key.equalsIgnoreCase("domain")) {
cookie.setDomain(value);
} else if (key.equalsIgnoreCase("max-age")) {
cookie.setMaxAge(Integer.parseInt(value));
} else if (key.equalsIgnoreCase("expires")) {
cookie.setExpires(DateUtils.parseDate(value));
} else if (key.equalsIgnoreCase("discard")) {
cookie.setDiscard(true);
} else if (key.equalsIgnoreCase("secure")) {
cookie.setSecure(true);
} else if (key.equalsIgnoreCase("httpOnly")) {
cookie.setHttpOnly(true);
} else if (key.equalsIgnoreCase("version")) {
cookie.setVersion(Integer.parseInt(value));
} else if (key.equalsIgnoreCase("comment")) {
cookie.setComment(value);
} else if (key.equalsIgnoreCase("samesite")) {
cookie.setSameSite(true);
cookie.setSameSiteMode(value);
}
//otherwise ignore this key-value pair
}
/**
/**
* Parses the cookies from a list of "Cookie:" header values. The cookie header values are parsed according to RFC2109 that
* defines the following syntax:
*
*
*
* cookie = "Cookie:" cookie-version
* 1*((";" | ",") cookie-value)
* cookie-value = NAME "=" VALUE [";" path] [";" domain]
* cookie-version = "$Version" "=" value
* NAME = attr
* VALUE = value
* path = "$Path" "=" value
* domain = "$Domain" "=" value
*
*
*
* @param maxCookies The maximum number of cookies. Used to prevent hash collision attacks
* @param allowEqualInValue if true equal characters are allowed in cookie values
* @param cookies The cookie values to parse
* @return A pared cookie map
*
* @see Cookie
* @see rfc2109
* @deprecated use {@link #parseRequestCookies(int, boolean, List, Set)} instead
*/
@Deprecated(since="2.2.0", forRemoval=true)
public static Map parseRequestCookies(int maxCookies, boolean allowEqualInValue, List cookies) {
return parseRequestCookies(maxCookies, allowEqualInValue, cookies, LegacyCookieSupport.COMMA_IS_SEPARATOR);
}
public static void parseRequestCookies(int maxCookies, boolean allowEqualInValue, List cookies, Set parsedCookies) {
parseRequestCookies(maxCookies, allowEqualInValue, cookies, parsedCookies, LegacyCookieSupport.COMMA_IS_SEPARATOR);
}
@Deprecated
static Map parseRequestCookies(int maxCookies, boolean allowEqualInValue, List cookies, boolean commaIsSeperator) {
return parseRequestCookies(maxCookies, allowEqualInValue, cookies, commaIsSeperator, LegacyCookieSupport.ALLOW_HTTP_SEPARATORS_IN_V0);
}
static void parseRequestCookies(int maxCookies, boolean allowEqualInValue, List cookies, Set parsedCookies, boolean commaIsSeperator) {
parseRequestCookies(maxCookies, allowEqualInValue, cookies, parsedCookies, commaIsSeperator, LegacyCookieSupport.ALLOW_HTTP_SEPARATORS_IN_V0);
}
static Map parseRequestCookies(int maxCookies, boolean allowEqualInValue, List cookies, boolean commaIsSeperator, boolean allowHttpSepartorsV0) {
if (cookies == null) {
return new TreeMap<>();
}
final Set parsedCookies = new HashSet<>();
for (String cookie : cookies) {
parseCookie(cookie, parsedCookies, maxCookies, allowEqualInValue, commaIsSeperator, allowHttpSepartorsV0);
}
final Map retVal = new TreeMap<>();
for (Cookie cookie : parsedCookies) {
retVal.put(cookie.getName(), cookie);
}
return retVal;
}
static void parseRequestCookies(int maxCookies, boolean allowEqualInValue, List cookies, Set parsedCookies, boolean commaIsSeperator, boolean allowHttpSepartorsV0) {
if (cookies != null) {
for (String cookie : cookies) {
parseCookie(cookie, parsedCookies, maxCookies, allowEqualInValue, commaIsSeperator, allowHttpSepartorsV0);
}
}
}
private static void parseCookie(final String cookie, final Set parsedCookies, int maxCookies, boolean allowEqualInValue, boolean commaIsSeperator, boolean allowHttpSepartorsV0) {
int state = 0;
String name = null;
int start = 0;
boolean containsEscapedQuotes = false;
int cookieCount = parsedCookies.size();
final Map cookies = new HashMap<>();
final Map additional = new HashMap<>();
for (int i = 0; i < cookie.length(); ++i) {
char c = cookie.charAt(i);
switch (state) {
case 0: {
//eat leading whitespace
if (c == ' ' || c == '\t' || c == ';') {
start = i + 1;
break;
}
state = 1;
//fall through
}
case 1: {
//extract key
if (c == '=') {
name = cookie.substring(start, i);
start = i + 1;
state = 2;
} else if (c == ';' || (commaIsSeperator && c == ',')) {
if(name != null) {
cookieCount = createCookie(name, cookie.substring(start, i), maxCookies, cookieCount, cookies, additional);
} else if(UndertowLogger.REQUEST_LOGGER.isTraceEnabled()) {
UndertowLogger.REQUEST_LOGGER.trace("Ignoring invalid cookies in header " + cookie);
}
state = 0;
start = i + 1;
}
break;
}
case 2: {
//extract value
if (c == ';' || (commaIsSeperator && c == ',')) {
cookieCount = createCookie(name, cookie.substring(start, i), maxCookies, cookieCount, cookies, additional);
state = 0;
start = i + 1;
} else if (c == '"' && start == i) { //only process the " if it is the first character
containsEscapedQuotes = false;
state = 3;
start = i + 1;
} else if (c == '=') {
if (!allowEqualInValue && !allowHttpSepartorsV0) {
cookieCount = createCookie(name, cookie.substring(start, i), maxCookies, cookieCount, cookies, additional);
state = 4;
start = i + 1;
}
} else if (c != ':' && !allowHttpSepartorsV0 && LegacyCookieSupport.isHttpSeparator(c)) {
// http separators are not allowed in V0 cookie value unless io.undertow.legacy.cookie.ALLOW_HTTP_SEPARATORS_IN_V0 is set to true.
// However, ":" (e.g. master:node1) is added as jvmRoute (instance-id) by default in WildFly domain mode.
// Though ":" is http separator, we allow it by default. Because, when Undertow runs as a proxy server (mod_cluster),
// we need to handle jvmRoute containing ":" in the request cookie value correctly to maintain the sticky session.
cookieCount = createCookie(name, cookie.substring(start, i), maxCookies, cookieCount, cookies, additional);
state = 4;
start = i + 1;
}
break;
}
case 3: {
//extract quoted value
if (c == '"') {
cookieCount = createCookie(name, containsEscapedQuotes ? unescapeDoubleQuotes(cookie.substring(start, i)) : cookie.substring(start, i), maxCookies, cookieCount, cookies, additional);
state = 0;
start = i + 1;
} else if (c == ';' || (commaIsSeperator && c == ',')) {
state = 0;
start = i + 1;
}
// Skip the next double quote char '"' when it is escaped by backslash '\' (i.e. \") inside the quoted value
if (c == '\\' && (i + 1 < cookie.length()) && cookie.charAt(i + 1) == '"') {
// But..., do not skip at the following conditions
if (i + 2 == cookie.length()) { // Cookie: key="\" or Cookie: key="...\"
break;
}
if (i + 2 < cookie.length() && (cookie.charAt(i + 2) == ';' // Cookie: key="\"; key2=...
|| (commaIsSeperator && cookie.charAt(i + 2) == ','))) { // Cookie: key="\", key2=...
break;
}
// Skip the next double quote char ('"' behind '\') in the cookie value
i++;
containsEscapedQuotes = true;
}
break;
}
case 4: {
//skip value portion behind '='
if (c == ';' || (commaIsSeperator && c == ',')) {
state = 0;
}
start = i + 1;
break;
}
}
}
if (state == 2) {
createCookie(name, cookie.substring(start), maxCookies, cookieCount, cookies, additional);
}
for (final Map.Entry entry : cookies.entrySet()) {
Cookie c = new CookieImpl(entry.getKey(), entry.getValue());
String domain = additional.get(DOMAIN);
if (domain != null) {
c.setDomain(domain);
}
String version = additional.get(VERSION);
if (version != null) {
c.setVersion(Integer.parseInt(version));
}
String path = additional.get(PATH);
if (path != null) {
c.setPath(path);
}
parsedCookies.add(c);
}
// RFC 6265 treats the domain, path and version attributes of an RFC 2109 cookie as a separate cookies
for (final Map.Entry entry : additional.entrySet()) {
if (DOMAIN.equals(entry.getKey())) {
Cookie c = new CookieImpl(DOMAIN, entry.getValue());
parsedCookies.add(c);
} else if (PATH.equals(entry.getKey())) {
Cookie c = new CookieImpl(PATH, entry.getValue());
parsedCookies.add(c);
} else if (VERSION.equals(entry.getKey())) {
Cookie c = new CookieImpl(VERSION, entry.getValue());
parsedCookies.add(c);
}
}
}
private static int createCookie(final String name, final String value, int maxCookies, int cookieCount,
final Map cookies, final Map additional) {
if (!name.isEmpty() && name.charAt(0) == '$') {
if(additional.containsKey(name)) {
return cookieCount;
}
additional.put(name, value);
return cookieCount;
} else {
if (cookieCount == maxCookies) {
throw UndertowMessages.MESSAGES.tooManyCookies(maxCookies);
}
if(cookies.containsKey(name)) {
return cookieCount;
}
cookies.put(name, value);
return ++cookieCount;
}
}
private static String unescapeDoubleQuotes(final String value) {
if (value == null || value.isEmpty()) {
return value;
}
// Replace all escaped double quote (\") to double quote (")
char[] tmp = new char[value.length()];
int dest = 0;
for(int i = 0; i < value.length(); i++) {
if (value.charAt(i) == '\\' && (i + 1 < value.length()) && value.charAt(i + 1) == '"') {
i++;
}
tmp[dest] = value.charAt(i);
dest++;
}
return new String(tmp, 0, dest);
}
private static final String CRUMB_SEPARATOR = "; ";
/**
* Cookie headers form: https://www.rfc-editor.org/rfc/rfc6265#section-4.2.1
* If more than one header entry exist for "Cookie", it will be assembled into one that conforms to rfc.
* @param headerMap
* @return
*/
public static void assembleCrumbs(final HeaderMap headerMap) {
final HeaderValues cookieValues = headerMap.get(Headers.COOKIE);
if (cookieValues != null && cookieValues.size() > 1) {
final StringBuilder oreos = new StringBuilder();
final String[] _cookieValues = cookieValues.toArray();
int slices = _cookieValues.length;
for (final String slice : _cookieValues) {
oreos.append(slice);
slices--;
if (slices >= 1) {
oreos.append(CRUMB_SEPARATOR);
}
}
cookieValues.clear();
cookieValues.add(oreos.toString());
}
}
/**
* IF there is single entry that follows RFC separation rules, it will be turned into singular fields. This should be only
* used PRIOR to compression.
*
* @param headerMap
*/
public static void disperseCrumbs(final HeaderMap headerMap) {
final HeaderValues cookieValues = headerMap.get(Headers.COOKIE);
// NOTE: If cookies are up2standard, thats the only case
// otherwise something is up, dont touch it
if (cookieValues != null && cookieValues.size() == 1) {
if (cookieValues.getFirst().contains(CRUMB_SEPARATOR)) {
final String[] cookieJar = cookieValues.getFirst().split(CRUMB_SEPARATOR);
headerMap.remove(Headers.COOKIE);
for (final String crumb : cookieJar) {
headerMap.addLast(Headers.COOKIE, crumb);
}
}
}
}
/**
* Fetch list containing crumbs( singular entries of Cookie header )
* @param headerMap
* @return
*/
public static List getCrumbs(final HeaderMap headerMap) {
final HeaderValues cookieValues = headerMap.get(Headers.COOKIE);
if (cookieValues != null) {
if (cookieValues.size() == 1 && cookieValues.getFirst().contains(CRUMB_SEPARATOR)) {
final String[] cookieJar = cookieValues.getFirst().split(CRUMB_SEPARATOR);
return Arrays.asList(cookieJar);
} else {
return cookieValues;
}
} else {
return Collections.emptyList();
}
}
private static final String CRUMBS_ASSEMBLY_DISABLE = "io.undertow.server.protocol.http.DisableCookieCrumbsAssembly";
private static final Boolean CRUMBS_ASSEMBLY_DISABLED = Boolean.valueOf(SecurityActions.getSystemProperty(CRUMBS_ASSEMBLY_DISABLE, "false"));
public static boolean isCrumbsAssemplyDisabled() {
return Cookies.CRUMBS_ASSEMBLY_DISABLED.booleanValue();
}
private Cookies() {
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy