Fix/jetty 10 9334 review cookie cutter (#9339)

Cookie cleanup
+ New Cookie parser with clearer focus on RFC6265.
+ Better compliance modes for RFC2965
+ Introduced CookieParser interface so that old and new parsers can coexist and be selected by compliance mode.

---------

Signed-off-by: Simone Bordet <simone.bordet@gmail.com>
Signed-off-by: Greg Wilkins <gregw@webtide.com>
Co-authored-by: Greg Wilkins <gregw@webtide.com>
Co-authored-by: Simone Bordet <simone.bordet@gmail.com>
This commit is contained in:
Simone Bordet 2023-02-14 22:38:24 +01:00 committed by GitHub
parent c2a9768d25
commit 7b8c2c1bf0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 1226 additions and 98 deletions

View File

@ -25,8 +25,10 @@ import org.slf4j.LoggerFactory;
import static java.util.Collections.unmodifiableSet;
import static java.util.EnumSet.allOf;
import static java.util.EnumSet.complementOf;
import static java.util.EnumSet.copyOf;
import static java.util.EnumSet.noneOf;
import static java.util.EnumSet.of;
/**
* The compliance mode for Cookie handling.
@ -38,14 +40,61 @@ public class CookieCompliance implements ComplianceViolation.Mode
public enum Violation implements ComplianceViolation
{
/**
* Allow a comma as part of a cookie value
* A comma was found in a cookie value.
*
* @deprecated Use SPECIAL_CHARS_IN_QUOTES
*/
COMMA_NOT_VALID_OCTET("https://tools.ietf.org/html/rfc6265#section-4.1.1", "Comma not valid as cookie-octet or separator"),
@Deprecated
COMMA_NOT_VALID_OCTET("https://tools.ietf.org/html/rfc6265#section-4.2.1", "Comma not valid as cookie-octet or separator"),
/**
* Allow cookies to have $ prefixed reserved parameters
* A comma was found as separator between cookies.
*/
RESERVED_NAMES_NOT_DOLLAR_PREFIXED("https://tools.ietf.org/html/rfc6265#section-4.1.1", "Reserved names no longer use '$' prefix");
COMMA_SEPARATOR("https://www.rfc-editor.org/rfc/rfc2965.html", "Comma cookie separator"),
/**
* @deprecated no replacement because was mistakenly considered a violation
*/
@Deprecated
RESERVED_NAMES_NOT_DOLLAR_PREFIXED("https://tools.ietf.org/html/rfc6265#section-4.2.1", "Reserved name no longer use '$' prefix"),
/**
* Special characters were found in a quoted cookie value.
*/
SPECIAL_CHARS_IN_QUOTES("https://www.rfc-editor.org/rfc/rfc6265#section-4.2.1", "Special character cannot be quoted"),
/**
* A backslash was found in a quoted cookie value.
*/
ESCAPE_IN_QUOTES("https://www.rfc-editor.org/rfc/rfc2616#section-2.2", "Escaped character in quotes"),
/**
* Quotes are not balanced or are embedded in value.
*/
BAD_QUOTES("https://www.rfc-editor.org/rfc/rfc2616#section-2.2", "Bad quotes"),
/**
* An invalid cookie was found, without a more specific violation.
* When this violation is not allowed, an exception is thrown.
*/
INVALID_COOKIES("https://tools.ietf.org/html/rfc6265", "Invalid cookie"),
/**
* A cookie attribute was found.
* The attribute value is retained only if {@link #ATTRIBUTE_VALUES} is allowed.
*/
ATTRIBUTES("https://www.rfc-editor.org/rfc/rfc6265#section-4.2.1", "Cookie attribute"),
/**
* A cookie attribute value was found and its value is retained.
* Allowing {@code ATTRIBUTE_VALUE} implies allowing {@link #ATTRIBUTES}.
*/
ATTRIBUTE_VALUES("https://www.rfc-editor.org/rfc/rfc6265#section-4.2.1", "Cookie attribute value"),
/**
* Whitespace was found around the cookie name and/or around the cookie value.
*/
OPTIONAL_WHITE_SPACE("https://www.rfc-editor.org/rfc/rfc6265#section-5.2", "White space around name/value");
private final String url;
private final String description;
@ -75,17 +124,58 @@ public class CookieCompliance implements ComplianceViolation.Mode
}
}
/**
* <p>A CookieCompliance mode that enforces <a href="https://tools.ietf.org/html/rfc6265">RFC 6265</a> compliance,
* but allows:</p>
* <ul>
* <li>{@link Violation#ATTRIBUTES}</li>
* <li>{@link Violation#INVALID_COOKIES}</li>
* <li>{@link Violation#OPTIONAL_WHITE_SPACE}</li>
* </ul>
*/
public static final CookieCompliance RFC6265 = new CookieCompliance("RFC6265", of(
Violation.ATTRIBUTES, Violation.INVALID_COOKIES, Violation.OPTIONAL_WHITE_SPACE)
);
/**
* A CookieCompliance mode that enforces <a href="https://tools.ietf.org/html/rfc6265">RFC 6265</a> compliance.
*/
public static final CookieCompliance RFC6265 = new CookieCompliance("RFC6265", noneOf(Violation.class));
public static final CookieCompliance RFC6265_STRICT = new CookieCompliance("RFC6265_STRICT", noneOf(Violation.class));
/**
* <p>A CookieCompliance mode that enforces <a href="https://tools.ietf.org/html/rfc6265">RFC 6265</a> compliance,
* but allows:</p>
* <ul>
* <li>{@link Violation#BAD_QUOTES}</li>
* <li>{@link Violation#ESCAPE_IN_QUOTES}</li>
* <li>{@link Violation#INVALID_COOKIES}</li>
* <li>{@link Violation#OPTIONAL_WHITE_SPACE}</li>
* <li>{@link Violation#SPECIAL_CHARS_IN_QUOTES}</li>
* </ul>
*/
public static final CookieCompliance RFC6265_LEGACY = new CookieCompliance("RFC6265_LEGACY", of(
Violation.BAD_QUOTES, Violation.ESCAPE_IN_QUOTES, Violation.INVALID_COOKIES, Violation.OPTIONAL_WHITE_SPACE, Violation.SPECIAL_CHARS_IN_QUOTES)
);
/**
* A CookieCompliance mode that allows <a href="https://tools.ietf.org/html/rfc2965">RFC 2965</a> compliance.
*/
public static final CookieCompliance RFC2965 = new CookieCompliance("RFC2965", allOf(Violation.class));
public static final CookieCompliance RFC2965_LEGACY = new CookieCompliance("RFC2965_LEGACY", allOf(Violation.class));
private static final List<CookieCompliance> KNOWN_MODES = Arrays.asList(RFC6265, RFC2965);
/**
* A CookieCompliance mode that allows <a href="https://tools.ietf.org/html/rfc2965">RFC 2965</a> compliance
* but does <b>not</b> allow:
* <ul>
* <li>{@link Violation#BAD_QUOTES}</li>
* <li>{@link Violation#COMMA_NOT_VALID_OCTET}</li>
* <li>{@link Violation#RESERVED_NAMES_NOT_DOLLAR_PREFIXED}</li>
* </ul>
*/
public static final CookieCompliance RFC2965 = new CookieCompliance("RFC2965", complementOf(of(
Violation.BAD_QUOTES, Violation.COMMA_NOT_VALID_OCTET, Violation.RESERVED_NAMES_NOT_DOLLAR_PREFIXED)
));
private static final List<CookieCompliance> KNOWN_MODES = Arrays.asList(RFC6265, RFC6265_STRICT, RFC6265_LEGACY, RFC2965, RFC2965_LEGACY);
private static final AtomicInteger __custom = new AtomicInteger();
public static CookieCompliance valueOf(String name)
@ -166,7 +256,7 @@ public class CookieCompliance implements ComplianceViolation.Mode
private final String _name;
private final Set<Violation> _violations;
private CookieCompliance(String name, Set<Violation> violations)
public CookieCompliance(String name, Set<Violation> violations)
{
_name = name;
_violations = unmodifiableSet(copyOf(Objects.requireNonNull(violations)));
@ -195,4 +285,9 @@ public class CookieCompliance implements ComplianceViolation.Mode
{
return _violations;
}
public boolean compliesWith(CookieCompliance mode)
{
return this == mode || getAllowed().containsAll(mode.getAllowed());
}
}

View File

@ -13,32 +13,47 @@
package org.eclipse.jetty.http;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.eclipse.jetty.http.CookieCompliance.Violation.BAD_QUOTES;
import static org.eclipse.jetty.http.CookieCompliance.Violation.COMMA_NOT_VALID_OCTET;
import static org.eclipse.jetty.http.CookieCompliance.Violation.ESCAPE_IN_QUOTES;
import static org.eclipse.jetty.http.CookieCompliance.Violation.INVALID_COOKIES;
import static org.eclipse.jetty.http.CookieCompliance.Violation.RESERVED_NAMES_NOT_DOLLAR_PREFIXED;
import static org.eclipse.jetty.http.CookieCompliance.Violation.SPECIAL_CHARS_IN_QUOTES;
/**
* Cookie parser
*/
public abstract class CookieCutter
@Deprecated
public class CookieCutter implements CookieParser
{
protected static final Logger LOG = LoggerFactory.getLogger(CookieCutter.class);
protected final CookieCompliance _complianceMode;
private final CookieParser.Handler _handler;
private final CookieCompliance _complianceMode;
private final ComplianceViolation.Listener _complianceListener;
protected CookieCutter(CookieCompliance compliance, ComplianceViolation.Listener complianceListener)
public CookieCutter(CookieParser.Handler handler, CookieCompliance compliance, ComplianceViolation.Listener complianceListener)
{
_handler = handler;
_complianceMode = compliance;
_complianceListener = complianceListener;
}
protected void parseFields(List<String> rawFields)
@Override
public void parseField(String field)
{
parseFields(Collections.singletonList(field));
}
@Override
public void parseFields(List<String> rawFields)
{
StringBuilder unquoted = null;
@ -93,19 +108,33 @@ public abstract class CookieCutter
break;
case '\\':
if (_complianceMode.allows(ESCAPE_IN_QUOTES))
reportComplianceViolation(ESCAPE_IN_QUOTES, hdr);
else
reject = true;
escaped = true;
continue;
case 0:
// unterminated quote, let's ignore quotes
if (_complianceMode.allows(BAD_QUOTES))
reportComplianceViolation(BAD_QUOTES, hdr);
else
reject = true;
unquoted.setLength(0);
inQuoted = false;
i--;
continue;
default:
if (isRFC6265RejectedCharacter(c))
{
if (_complianceMode.allows(SPECIAL_CHARS_IN_QUOTES))
reportComplianceViolation(SPECIAL_CHARS_IN_QUOTES, hdr);
else
reject = true;
}
unquoted.append(c);
continue;
}
}
else
@ -128,6 +157,10 @@ public abstract class CookieCutter
if (quoted)
{
// must have been a bad internal quote. let's fix as best we can
if (_complianceMode.allows(BAD_QUOTES))
reportComplianceViolation(BAD_QUOTES, hdr);
else
reject = true;
unquoted.append(hdr, tokenstart, i--);
inQuoted = true;
quoted = false;
@ -157,7 +190,7 @@ public abstract class CookieCutter
try
{
if (name.startsWith("$"))
if (name != null && name.startsWith("$"))
{
if (RESERVED_NAMES_NOT_DOLLAR_PREFIXED.isAllowedBy(_complianceMode))
{
@ -187,11 +220,18 @@ public abstract class CookieCutter
// This is a new cookie, so add the completed last cookie if we have one
if (cookieName != null)
{
if (!reject)
if (reject)
{
addCookie(cookieName, cookieValue, cookieDomain, cookiePath, cookieVersion, cookieComment);
reject = false;
if (_complianceMode.allows(INVALID_COOKIES))
reportComplianceViolation(INVALID_COOKIES, hdr);
else
throw new IllegalArgumentException("Bad Cookie");
}
else
{
_handler.addCookie(cookieName, cookieValue, cookieVersion, cookieDomain, cookiePath, cookieComment);
}
reject = false;
cookieDomain = null;
cookiePath = null;
cookieComment = null;
@ -227,24 +267,27 @@ public abstract class CookieCutter
if (quoted)
{
// must have been a bad internal quote. let's fix as best we can
if (_complianceMode.allows(BAD_QUOTES))
reportComplianceViolation(BAD_QUOTES, hdr);
else
reject = true;
unquoted.append(hdr, tokenstart, i--);
inQuoted = true;
quoted = false;
continue;
}
if (_complianceMode == CookieCompliance.RFC6265)
if (isRFC6265RejectedCharacter(c))
{
if (isRFC6265RejectedCharacter(inQuoted, c))
{
if (c < 128 && _complianceMode.allows(SPECIAL_CHARS_IN_QUOTES))
reportComplianceViolation(SPECIAL_CHARS_IN_QUOTES, hdr);
else
reject = true;
}
}
if (tokenstart < 0)
tokenstart = i;
tokenend = i;
continue;
}
}
else
@ -287,68 +330,69 @@ public abstract class CookieCutter
if (quoted)
{
// must have been a bad internal quote. let's fix as best we can
if (_complianceMode.allows(BAD_QUOTES))
reportComplianceViolation(BAD_QUOTES, hdr);
else
reject = true;
unquoted.append(hdr, tokenstart, i--);
inQuoted = true;
quoted = false;
continue;
}
if (_complianceMode == CookieCompliance.RFC6265)
if (isRFC6265RejectedCharacter(c))
{
if (isRFC6265RejectedCharacter(inQuoted, c))
{
if (_complianceMode.allows(SPECIAL_CHARS_IN_QUOTES))
reportComplianceViolation(SPECIAL_CHARS_IN_QUOTES, hdr);
else
reject = true;
}
}
if (tokenstart < 0)
tokenstart = i;
tokenend = i;
continue;
}
}
}
}
if (cookieName != null && !reject)
addCookie(cookieName, cookieValue, cookieDomain, cookiePath, cookieVersion, cookieComment);
if (cookieName != null)
{
if (reject)
{
if (_complianceMode.allows(INVALID_COOKIES))
reportComplianceViolation(INVALID_COOKIES, hdr);
else
throw new IllegalArgumentException("Bad Cookie");
}
else
{
_handler.addCookie(cookieName, cookieValue, cookieVersion, cookieDomain, cookiePath, cookieComment);
}
}
}
}
protected void reportComplianceViolation(CookieCompliance.Violation violation, String reason)
{
if (_complianceListener != null)
{
_complianceListener.onComplianceViolation(_complianceMode, violation, reason);
}
}
protected abstract void addCookie(String cookieName, String cookieValue, String cookieDomain, String cookiePath, int cookieVersion, String cookieComment);
protected boolean isRFC6265RejectedCharacter(boolean inQuoted, char c)
protected boolean isRFC6265RejectedCharacter(char c)
{
if (inQuoted)
{
// We only reject if a Control Character is encountered
if (Character.isISOControl(c))
{
return true;
}
}
else
{
/* From RFC6265 - Section 4.1.1 - Syntax
* cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
* ; US-ASCII characters excluding CTLs,
* ; whitespace DQUOTE, comma, semicolon,
* ; and backslash
*/
return Character.isISOControl(c) || // control characters
c > 127 || // 8-bit characters
c == ',' || // comma
c == ';'; // semicolon
}
return false;
/* From RFC6265 - Section 4.1.1 - Syntax
* cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
* ; US-ASCII characters excluding CTLs,
* ; whitespace DQUOTE, comma, semicolon,
* ; and backslash
*/
return Character.isISOControl(c) || // control characters
c > 127 || // 8-bit characters
c == ' ' || // whitespace
c == '"' || // DQUOTE
c == ',' || // comma
c == ';' || // semicolon
c == '\\'; // backslash
}
}

View File

@ -0,0 +1,60 @@
//
// ========================================================================
// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.http;
import java.util.List;
import static org.eclipse.jetty.http.CookieCompliance.Violation.BAD_QUOTES;
import static org.eclipse.jetty.http.CookieCompliance.Violation.Listener;
/**
* <p>Cookie parser.</p>
* <p>An interface for variations of a cookie parser.</p>
*/
public interface CookieParser
{
/**
* <p>A factory method to create a new parser suitable for the compliance mode.</p>
* @param compliance The compliance mode to use for parsing.
* @param complianceListener A listener for compliance violations or null.
* @return A CookieParser instance.
*/
static CookieParser newParser(Handler handler, CookieCompliance compliance, Listener complianceListener)
{
// The RFC6265CookieParser is primarily a RFC6265 parser, but it can handle most
// defined "violations" so that it effectively becomes a RFC2965 parser. However, it
// cannot forgive bad quotes. Thus, we use the legacy CookieCutter parser only if
// the compliance mode requires BAD QUOTES.
if (compliance.allows(BAD_QUOTES))
return new CookieCutter(handler, compliance, complianceListener);
return new RFC6265CookieParser(handler, compliance, complianceListener);
}
void parseField(String field);
default void parseFields(List<String> rawFields)
{
// For each cookie field
for (String field : rawFields)
parseField(field);
}
/**
* The handler of parsed cookies.
*/
interface Handler
{
void addCookie(String name, String value, int version, String domain, String path, String comment);
}
}

View File

@ -281,11 +281,9 @@ public class HttpCookie
public String getSetCookie(CookieCompliance compliance)
{
if (compliance == CookieCompliance.RFC6265)
if (CookieCompliance.RFC6265.compliesWith(compliance))
return getRFC6265SetCookie();
if (compliance == CookieCompliance.RFC2965)
return getRFC2965SetCookie();
throw new IllegalStateException();
return getRFC2965SetCookie();
}
public String getRFC2965SetCookie()

View File

@ -441,8 +441,7 @@ public class HttpParser
private HttpTokens.Token next(ByteBuffer buffer)
{
byte ch = buffer.get();
HttpTokens.Token t = HttpTokens.TOKENS[0xff & ch];
HttpTokens.Token t = HttpTokens.getToken(ch);
switch (t.getType())
{

View File

@ -47,6 +47,16 @@ public class HttpTokens
OTEXT // Obsolete text
}
public static Token getToken(byte b)
{
return TOKENS[0xFF & b];
}
public static Token getToken(char c)
{
return c <= 0xFF ? TOKENS[c] : null;
}
public static class Token
{
private final Type _type;
@ -54,6 +64,10 @@ public class HttpTokens
private final char _c;
private final int _x;
private final boolean _rfc2616Token;
private final boolean _rfc6265CookieOctet;
private Token(byte b, Type type)
{
_type = type;
@ -61,6 +75,30 @@ public class HttpTokens
_c = (char)(0xff & b);
char lc = (_c >= 'A' & _c <= 'Z') ? ((char)(_c - 'A' + 'a')) : _c;
_x = (_type == Type.DIGIT || _type == Type.ALPHA && lc >= 'a' && lc <= 'f') ? TypeUtil.convertHexDigit(b) : -1;
// token = 1*<any CHAR except CTLs or separators>
// separators = "(" | ")" | "<" | ">" | "@"
// | "," | ";" | ":" | "\" | <">
// | "/" | "[" | "]" | "?" | "="
// | "{" | "}" | SP | HT
// CTL = <any US-ASCII control character
// (octets 0 - 31) and DEL (127)>
_rfc2616Token = b >= 32 && b < 127 &&
b != '(' && b != ')' && b != '<' && b != '>' && b != '@' &&
b != ',' && b != ';' && b != ':' && b != '\\' && b != '"' &&
b != '/' && b != '[' && b != ']' && b != '?' && b != '=' &&
b != '{' && b != '}' && b != ' ';
// cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
// ; US-ASCII characters excluding CTLs,
// ; whitespace DQUOTE, comma, semicolon,
// ; and backslash
_rfc6265CookieOctet =
b == 0x21 ||
b >= 0x23 && b <= 0x2B ||
b >= 0x2D && b <= 0x3A ||
b >= 0x3C && b <= 0x5B ||
b >= 0x5D && b <= 0x7E;
}
public Type getType()
@ -83,6 +121,16 @@ public class HttpTokens
return _x >= 0;
}
public boolean isRfc2616Token()
{
return _rfc2616Token;
}
public boolean isRfc6265CookieOctet()
{
return _rfc6265CookieOctet;
}
public int getHexDigit()
{
return _x;

View File

@ -0,0 +1,421 @@
//
// ========================================================================
// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.http;
import java.util.Locale;
import org.eclipse.jetty.util.StringUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.eclipse.jetty.http.CookieCompliance.Violation.ATTRIBUTES;
import static org.eclipse.jetty.http.CookieCompliance.Violation.ATTRIBUTE_VALUES;
import static org.eclipse.jetty.http.CookieCompliance.Violation.COMMA_NOT_VALID_OCTET;
import static org.eclipse.jetty.http.CookieCompliance.Violation.COMMA_SEPARATOR;
import static org.eclipse.jetty.http.CookieCompliance.Violation.ESCAPE_IN_QUOTES;
import static org.eclipse.jetty.http.CookieCompliance.Violation.INVALID_COOKIES;
import static org.eclipse.jetty.http.CookieCompliance.Violation.OPTIONAL_WHITE_SPACE;
import static org.eclipse.jetty.http.CookieCompliance.Violation.SPECIAL_CHARS_IN_QUOTES;
/**
* Cookie parser
*/
public class RFC6265CookieParser implements CookieParser
{
protected static final Logger LOG = LoggerFactory.getLogger(RFC6265CookieParser.class);
private final CookieParser.Handler _handler;
private final CookieCompliance _complianceMode;
private final ComplianceViolation.Listener _complianceListener;
protected RFC6265CookieParser(CookieParser.Handler handler, CookieCompliance compliance, ComplianceViolation.Listener complianceListener)
{
_handler = handler;
_complianceMode = compliance;
_complianceListener = complianceListener;
}
private enum State
{
START,
IN_NAME,
AFTER_NAME,
VALUE,
IN_VALUE,
IN_QUOTED_VALUE,
ESCAPED_VALUE,
AFTER_QUOTED_VALUE,
END,
INVALID_COOKIE
}
@Override
public void parseField(String field)
{
State state = State.START;
String attributeName = null;
String value = null;
String cookieName = null;
String cookieValue = null;
String cookiePath = null;
String cookieDomain = null;
String cookieComment = null;
int cookieVersion = 0;
boolean cookieInvalid = false;
int length = field.length();
StringBuilder string = new StringBuilder();
for (int i = 0; i <= length; i++)
{
char c = i == length ? ';' : field.charAt(i);
HttpTokens.Token token = HttpTokens.getToken(c);
if (token == null)
{
if (!_complianceMode.allows(INVALID_COOKIES))
throw new IllegalArgumentException("Invalid Cookie character");
state = State.INVALID_COOKIE;
continue;
}
switch (state)
{
case START:
if (c == ' ' || c == '\t' || c == ';')
continue;
string.setLength(0);
if (token.isRfc2616Token())
{
if (!StringUtil.isBlank(cookieName) && c != '$')
{
_handler.addCookie(cookieName, cookieValue, cookieVersion, cookieDomain, cookiePath, cookieComment);
cookieName = null;
cookieValue = null;
cookieDomain = null;
cookiePath = null;
cookieComment = null;
}
string.append(c);
state = State.IN_NAME;
}
else if (_complianceMode.allows(INVALID_COOKIES))
{
reportComplianceViolation(INVALID_COOKIES, field);
state = State.INVALID_COOKIE;
}
else
{
throw new IllegalArgumentException("Bad Cookie name");
}
break;
case IN_NAME:
if (c == '=')
{
if (string.charAt(0) == '$')
attributeName = string.toString();
else
cookieName = string.toString();
state = State.VALUE;
continue;
}
if ((c == ' ' || c == '\t') && _complianceMode.allows(OPTIONAL_WHITE_SPACE))
{
reportComplianceViolation(OPTIONAL_WHITE_SPACE, field);
if (string.charAt(0) == '$')
attributeName = string.toString();
else
cookieName = string.toString();
state = State.AFTER_NAME;
continue;
}
if (token.isRfc2616Token())
{
string.append(c);
}
else if (_complianceMode.allows(INVALID_COOKIES))
{
reportComplianceViolation(INVALID_COOKIES, field);
state = c == ';' ? State.START : State.INVALID_COOKIE;
}
else
{
throw new IllegalArgumentException("Bad Cookie name");
}
break;
case AFTER_NAME:
if (c == '=')
{
state = State.VALUE;
continue;
}
if (c == ';' || c == ',')
{
state = State.START;
continue;
}
if (_complianceMode.allows(INVALID_COOKIES))
{
reportComplianceViolation(INVALID_COOKIES, field);
state = State.INVALID_COOKIE;
}
else
{
throw new IllegalArgumentException("Bad Cookie");
}
break;
case VALUE:
if (c == ' ' && _complianceMode.allows(OPTIONAL_WHITE_SPACE))
{
reportComplianceViolation(OPTIONAL_WHITE_SPACE, field);
continue;
}
string.setLength(0);
if (c == '"')
{
state = State.IN_QUOTED_VALUE;
}
else if (c == ';')
{
value = "";
i--;
state = State.END;
}
else if (token.isRfc6265CookieOctet())
{
string.append(c);
state = State.IN_VALUE;
}
else if (_complianceMode.allows(INVALID_COOKIES))
{
reportComplianceViolation(INVALID_COOKIES, field);
state = State.INVALID_COOKIE;
}
else
{
throw new IllegalArgumentException("Bad Cookie value");
}
break;
case IN_VALUE:
if (c == ';' || c == ',' || c == ' ' || c == '\t')
{
value = string.toString();
i--;
state = State.END;
}
else if (token.isRfc6265CookieOctet())
{
string.append(c);
}
else if (_complianceMode.allows(INVALID_COOKIES))
{
reportComplianceViolation(INVALID_COOKIES, field);
state = State.INVALID_COOKIE;
}
else
{
throw new IllegalArgumentException("Bad Cookie value");
}
break;
case IN_QUOTED_VALUE:
if (c == '"')
{
value = string.toString();
state = State.AFTER_QUOTED_VALUE;
}
else if (c == '\\' && _complianceMode.allows(ESCAPE_IN_QUOTES))
{
state = State.ESCAPED_VALUE;
}
else if (token.isRfc6265CookieOctet())
{
string.append(c);
}
else if (_complianceMode.allows(SPECIAL_CHARS_IN_QUOTES))
{
reportComplianceViolation(SPECIAL_CHARS_IN_QUOTES, field);
string.append(c);
}
else if (c == ',' && _complianceMode.allows(COMMA_NOT_VALID_OCTET))
{
reportComplianceViolation(COMMA_NOT_VALID_OCTET, field);
string.append(c);
}
else if (_complianceMode.allows(INVALID_COOKIES))
{
string.append(c);
if (!cookieInvalid)
{
cookieInvalid = true;
reportComplianceViolation(INVALID_COOKIES, field);
}
// Try to find the closing double quote by staying in the current state.
}
else
{
throw new IllegalArgumentException("Bad Cookie quoted value");
}
break;
case ESCAPED_VALUE:
string.append(c);
state = State.IN_QUOTED_VALUE;
break;
case AFTER_QUOTED_VALUE:
if (c == ';' || c == ',' || c == ' ' || c == '\t')
{
i--;
state = cookieInvalid ? State.INVALID_COOKIE : State.END;
}
else if (_complianceMode.allows(INVALID_COOKIES))
{
reportComplianceViolation(INVALID_COOKIES, field);
state = State.INVALID_COOKIE;
}
else
{
throw new IllegalArgumentException("Bad Cookie quoted value");
}
break;
case END:
if (c == ';')
{
state = State.START;
}
else if (c == ',')
{
if (_complianceMode.allows(COMMA_SEPARATOR))
{
reportComplianceViolation(COMMA_SEPARATOR, field);
state = State.START;
}
else if (_complianceMode.allows(INVALID_COOKIES))
{
reportComplianceViolation(INVALID_COOKIES, field);
state = State.INVALID_COOKIE;
continue;
}
else
{
throw new IllegalStateException("Comma cookie separator");
}
}
else if ((c == ' ' || c == '\t') && _complianceMode.allows(OPTIONAL_WHITE_SPACE))
{
reportComplianceViolation(OPTIONAL_WHITE_SPACE, field);
continue;
}
boolean knownAttribute = true;
if (StringUtil.isBlank(attributeName))
{
cookieValue = value;
}
else
{
// We have an attribute.
if (_complianceMode.allows(ATTRIBUTE_VALUES))
{
reportComplianceViolation(ATTRIBUTES, field);
switch (attributeName.toLowerCase(Locale.ENGLISH))
{
case "$path":
cookiePath = value;
break;
case "$domain":
cookieDomain = value;
break;
case "$port":
cookieComment = "$port=" + value;
break;
case "$version":
cookieVersion = Integer.parseInt(value);
break;
default:
knownAttribute = false;
break;
}
}
else if (_complianceMode.allows(ATTRIBUTES))
{
reportComplianceViolation(ATTRIBUTES, field);
}
else if (_complianceMode.allows(INVALID_COOKIES))
{
reportComplianceViolation(INVALID_COOKIES, field);
state = State.INVALID_COOKIE;
continue;
}
else
{
throw new IllegalArgumentException("Invalid Cookie with attributes");
}
attributeName = null;
}
value = null;
if (!knownAttribute)
{
if (!_complianceMode.allows(INVALID_COOKIES))
throw new IllegalArgumentException("Invalid Cookie attribute");
reportComplianceViolation(INVALID_COOKIES, field);
state = State.INVALID_COOKIE;
continue;
}
if (state == State.END)
throw new IllegalStateException("Invalid cookie");
break;
case INVALID_COOKIE:
attributeName = null;
value = null;
cookieName = null;
cookieValue = null;
cookiePath = null;
cookieDomain = null;
cookieComment = null;
cookieInvalid = false;
if (c == ';')
state = State.START;
break;
}
}
if (!cookieInvalid && !StringUtil.isBlank(cookieName))
_handler.addCookie(cookieName, cookieValue, cookieVersion, cookieDomain, cookiePath, cookieComment);
}
protected void reportComplianceViolation(CookieCompliance.Violation violation, String reason)
{
if (_complianceListener != null)
_complianceListener.onComplianceViolation(_complianceMode, violation, reason);
}
}

View File

@ -14,7 +14,6 @@
package org.eclipse.jetty.http;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Stream;
@ -64,6 +63,8 @@ public class CookieCutterLenientTest
// lenient with spaces and EOF
Arguments.of("abc=", "abc", ""),
Arguments.of("abc= ", "abc", ""),
Arguments.of("abc= x", "abc", "x"),
Arguments.of("abc = ", "abc", ""),
Arguments.of("abc = ;", "abc", ""),
Arguments.of("abc = ; ", "abc", ""),
@ -173,28 +174,27 @@ public class CookieCutterLenientTest
}
}
class TestCutter extends CookieCutter
static class TestCutter implements CookieParser.Handler
{
CookieCutter cutter;
List<String> names = new ArrayList<>();
List<String> values = new ArrayList<>();
protected TestCutter()
{
super(CookieCompliance.RFC6265, null);
}
@Override
protected void addCookie(String cookieName, String cookieValue, String cookieDomain, String cookiePath, int cookieVersion, String cookieComment)
{
names.add(cookieName);
values.add(cookieValue);
cutter = new CookieCutter(this, CookieCompliance.RFC6265_LEGACY, null);
}
public void parseField(String field)
{
super.parseFields(Collections.singletonList(field));
cutter.parseField(field);
}
@Override
public void addCookie(String cookieName, String cookieValue, int cookieVersion, String cookieDomain, String cookiePath, String cookieComment)
{
names.add(cookieName);
values.add(cookieValue);
}
}
;
}

View File

@ -54,7 +54,7 @@ public class CookieCutterTest
{
String rawCookie = "$Version=\"1\"; Customer=\"WILE_E_COYOTE\"; $Path=\"/acme\"";
Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC2965, rawCookie);
Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC2965_LEGACY, rawCookie);
assertThat("Cookies.length", cookies.length, is(1));
assertCookie("Cookies[0]", cookies[0], "Customer", "WILE_E_COYOTE", 1, "/acme");
@ -70,7 +70,7 @@ public class CookieCutterTest
"Customer=\"WILE_E_COYOTE\"; $Path=\"/acme\"; " +
"Part_Number=\"Rocket_Launcher_0001\"; $Path=\"/acme\"";
Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC2965, rawCookie);
Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC2965_LEGACY, rawCookie);
assertThat("Cookies.length", cookies.length, is(2));
assertCookie("Cookies[0]", cookies[0], "Customer", "WILE_E_COYOTE", 1, "/acme");
@ -88,7 +88,7 @@ public class CookieCutterTest
"Part_Number=\"Rocket_Launcher_0001\"; $Path=\"/acme\"; " +
"Shipping=\"FedEx\"; $Path=\"/acme\"";
Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC2965, rawCookie);
Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC2965_LEGACY, rawCookie);
assertThat("Cookies.length", cookies.length, is(3));
assertCookie("Cookies[0]", cookies[0], "Customer", "WILE_E_COYOTE", 1, "/acme");
@ -106,7 +106,7 @@ public class CookieCutterTest
"Part_Number=\"Riding_Rocket_0023\"; $Path=\"/acme/ammo\"; " +
"Part_Number=\"Rocket_Launcher_0001\"; $Path=\"/acme\"";
Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC2965, rawCookie);
Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC2965_LEGACY, rawCookie);
assertThat("Cookies.length", cookies.length, is(2));
assertCookie("Cookies[0]", cookies[0], "Part_Number", "Riding_Rocket_0023", 1, "/acme/ammo");
@ -123,7 +123,7 @@ public class CookieCutterTest
"session_id=\"1234\"; " +
"session_id=\"1111\"; $Domain=\".cracker.edu\"";
Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC2965, rawCookie);
Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC2965_LEGACY, rawCookie);
assertThat("Cookies.length", cookies.length, is(2));
assertCookie("Cookies[0]", cookies[0], "session_id", "1234", 1, null);
@ -139,13 +139,13 @@ public class CookieCutterTest
String rawCookie = "$Version=\"1\"; session_id=\"1234\", " +
"$Version=\"1\"; session_id=\"1111\"; $Domain=\".cracker.edu\"";
Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC2965, rawCookie);
Cookie[] cookies /* = parseCookieHeaders(CookieCompliance.RFC2965_LEGACY, rawCookie);
assertThat("Cookies.length", cookies.length, is(2));
assertCookie("Cookies[0]", cookies[0], "session_id", "1234", 1, null);
assertCookie("Cookies[1]", cookies[1], "session_id", "1111", 1, null);
cookies = parseCookieHeaders(CookieCompliance.RFC6265, rawCookie);
cookies */ = parseCookieHeaders(CookieCompliance.RFC6265_LEGACY, rawCookie);
assertThat("Cookies.length", cookies.length, is(2));
assertCookie("Cookies[0]", cookies[0], "session_id", "1234\", $Version=\"1", 0, null);
assertCookie("Cookies[1]", cookies[1], "session_id", "1111", 0, null);
@ -159,7 +159,7 @@ public class CookieCutterTest
{
String rawCookie = "SID=31d4d96e407aad42";
Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC6265, rawCookie);
Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC6265_LEGACY, rawCookie);
assertThat("Cookies.length", cookies.length, is(1));
assertCookie("Cookies[0]", cookies[0], "SID", "31d4d96e407aad42", 0, null);
@ -173,7 +173,7 @@ public class CookieCutterTest
{
String rawCookie = "SID=31d4d96e407aad42; lang=en-US";
Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC6265, rawCookie);
Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC6265_LEGACY, rawCookie);
assertThat("Cookies.length", cookies.length, is(2));
assertCookie("Cookies[0]", cookies[0], "SID", "31d4d96e407aad42", 0, null);
@ -188,7 +188,7 @@ public class CookieCutterTest
{
String rawCookie = "key=value";
Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC6265, rawCookie);
Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC6265_LEGACY, rawCookie);
assertThat("Cookies.length", cookies.length, is(1));
assertCookie("Cookies[0]", cookies[0], "key", "value", 0, null);
@ -202,7 +202,7 @@ public class CookieCutterTest
{
String rawCookie = "$key=value";
Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC6265, rawCookie);
Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC6265_LEGACY, rawCookie);
assertThat("Cookies.length", cookies.length, is(0));
}
@ -214,7 +214,7 @@ public class CookieCutterTest
// The first cookie "testcookie" should be ignored, per RFC6265, as it's missing the "=" sign.
Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC6265, rawCookie);
Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC6265_LEGACY, rawCookie);
assertThat("Cookies.length", cookies.length, is(2));
assertCookie("Cookies[0]", cookies[0], "server.id", "abcd", 0, null);
@ -228,7 +228,7 @@ public class CookieCutterTest
Arrays.fill(excessive, ';');
String rawCookie = "foo=bar; " + excessive + "; xyz=pdq";
Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC6265, rawCookie);
Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC6265_LEGACY, rawCookie);
assertThat("Cookies.length", cookies.length, is(2));
assertCookie("Cookies[0]", cookies[0], "foo", "bar", 0, null);
@ -285,24 +285,25 @@ public class CookieCutterTest
}
}
class TestCutter extends CookieCutter
class TestCutter implements CookieParser.Handler
{
List<Cookie> cookies = new ArrayList<>();
CookieCutter cutter;
public TestCutter(CookieCompliance compliance, ComplianceViolation.Listener complianceListener)
{
super(compliance, complianceListener);
cutter = new CookieCutter(this, compliance, complianceListener);
}
@Override
protected void addCookie(String cookieName, String cookieValue, String cookieDomain, String cookiePath, int cookieVersion, String cookieComment)
public void addCookie(String cookieName, String cookieValue, int cookieVersion, String cookieDomain, String cookiePath, String cookieComment)
{
cookies.add(new Cookie(cookieName, cookieValue, cookieDomain, cookiePath, cookieVersion, cookieComment));
}
public void parseFields(String... fields)
{
super.parseFields(Arrays.asList(fields));
cutter.parseFields(Arrays.asList(fields));
}
}
}

View File

@ -0,0 +1,37 @@
//
// ========================================================================
// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.http;
import java.util.EnumSet;
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.instanceOf;
public class CookieParserTest
{
@Test
public void testNewCookieParser()
{
assertThat(CookieParser.newParser(null, CookieCompliance.RFC2965_LEGACY, null), instanceOf(CookieCutter.class));
assertThat(CookieParser.newParser(null, CookieCompliance.RFC6265_LEGACY, null), instanceOf(CookieCutter.class));
assertThat(CookieParser.newParser(null, new CookieCompliance("custom", EnumSet.of(CookieCompliance.Violation.COMMA_SEPARATOR, CookieCompliance.Violation.BAD_QUOTES)), null), instanceOf(CookieCutter.class));
assertThat(CookieParser.newParser(null, CookieCompliance.RFC2965, null), instanceOf(RFC6265CookieParser.class));
assertThat(CookieParser.newParser(null, CookieCompliance.RFC6265, null), instanceOf(RFC6265CookieParser.class));
assertThat(CookieParser.newParser(null, CookieCompliance.RFC6265_STRICT, null), instanceOf(RFC6265CookieParser.class));
assertThat(CookieParser.newParser(null, new CookieCompliance("custom", EnumSet.of(CookieCompliance.Violation.COMMA_SEPARATOR)), null), instanceOf(RFC6265CookieParser.class));
}
}

View File

@ -0,0 +1,422 @@
//
// ========================================================================
// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.http;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertThrows;
public class RFC6265CookieParserTest
{
/**
* Example from RFC2109 and RFC2965
*/
@Test
public void testRFC2965Single()
{
String rawCookie = "$Version=\"1\"; Customer=\"WILE_E_COYOTE\"; $Path=\"/acme\"";
// Test with RFC 2965.
TestCookieParser parser = new TestCookieParser(CookieCompliance.RFC2965_LEGACY);
List<Cookie> cookies = parser.parseFields(rawCookie);
assertThat("Cookies.length", cookies.size(), is(1));
assertCookie("Cookies[0]", cookies.get(0), "Customer", "WILE_E_COYOTE", 1, "/acme");
// There are 2 attributes, so 2 violations.
assertThat(parser.violations.size(), is(2));
// Same test with RFC 6265.
parser = new TestCookieParser(CookieCompliance.RFC6265);
cookies = parser.parseFields(rawCookie);
assertThat("Cookies.length", cookies.size(), is(1));
assertCookie("Cookies[0]", cookies.get(0), "Customer", "WILE_E_COYOTE", 0, null);
// There are 2 attributes, so 2 violations.
assertThat(parser.violations.size(), is(2));
// Same test with RFC 6265 strict should throw.
TestCookieParser strictParser = new TestCookieParser(CookieCompliance.RFC6265_STRICT);
assertThrows(IllegalArgumentException.class, () -> strictParser.parseFields(rawCookie));
}
/**
* Example from RFC2109 and RFC2965
*/
@Test
public void testRFC2965Double()
{
String rawCookie = "$Version=\"1\"; " +
"Customer=\"WILE_E_COYOTE\"; $Path=\"/acme\"; " +
"Part_Number=\"Rocket_Launcher_0001\"; $Path=\"/acme\"";
Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC2965_LEGACY, rawCookie);
assertThat("Cookies.length", cookies.length, is(2));
assertCookie("Cookies[0]", cookies[0], "Customer", "WILE_E_COYOTE", 1, "/acme");
assertCookie("Cookies[1]", cookies[1], "Part_Number", "Rocket_Launcher_0001", 1, "/acme");
}
/**
* Example from RFC2109 and RFC2965
*/
@Test
public void testRFCTriple()
{
String rawCookie = "$Version=\"1\"; " +
"Customer=\"WILE_E_COYOTE\"; $Path=\"/acme\"; " +
"Part_Number=\"Rocket_Launcher_0001\"; $Path=\"/acme\"; " +
"Shipping=\"FedEx\"; $Path=\"/acme\"";
Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC2965_LEGACY, rawCookie);
assertThat("Cookies.length", cookies.length, is(3));
assertCookie("Cookies[0]", cookies[0], "Customer", "WILE_E_COYOTE", 1, "/acme");
assertCookie("Cookies[1]", cookies[1], "Part_Number", "Rocket_Launcher_0001", 1, "/acme");
assertCookie("Cookies[2]", cookies[2], "Shipping", "FedEx", 1, "/acme");
}
/**
* Example from RFC2109 and RFC2965
*/
@Test
public void testRFCPathExample()
{
String rawCookie = "$Version=\"1\"; " +
"Part_Number=\"Riding_Rocket_0023\"; $Path=\"/acme/ammo\"; " +
"Part_Number=\"Rocket_Launcher_0001\"; $Path=\"/acme\"";
Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC2965_LEGACY, rawCookie);
assertThat("Cookies.length", cookies.length, is(2));
assertCookie("Cookies[0]", cookies[0], "Part_Number", "Riding_Rocket_0023", 1, "/acme/ammo");
assertCookie("Cookies[1]", cookies[1], "Part_Number", "Rocket_Launcher_0001", 1, "/acme");
}
/**
* Example from RFC2109
*/
@Test
public void testRFC2109CookieSpoofingExample()
{
String rawCookie = "$Version=\"1\"; " +
"session_id=\"1234\"; " +
"session_id=\"1111\"; $Domain=\".cracker.edu\"";
Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC2965_LEGACY, rawCookie);
assertThat("Cookies.length", cookies.length, is(2));
assertCookie("Cookies[0]", cookies[0], "session_id", "1234", 1, null);
assertCookie("Cookies[1]", cookies[1], "session_id", "1111", 1, null);
}
/**
* Example from RFC2965
*/
@Test
public void testRFC2965CookieSpoofingExample()
{
String rawCookie = "$Version=\"1\"; session_id=\"1234\", " +
"$Version=\"1\"; session_id=\"1111\"; $Domain=\".cracker.edu\"";
Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC2965_LEGACY, rawCookie);
assertThat("Cookies.length", cookies.length, is(2));
assertCookie("Cookies[0]", cookies[0], "session_id", "1234", 1, null);
assertCookie("Cookies[1]", cookies[1], "session_id", "1111", 1, null);
cookies = parseCookieHeaders(CookieCompliance.RFC6265, rawCookie);
assertThat("Cookies.length", cookies.length, is(1));
assertCookie("Cookies[1]", cookies[0], "session_id", "1111", 0, null);
}
/**
* Example from RFC6265
*/
@Test
public void testRFC6265SidExample()
{
String rawCookie = "SID=31d4d96e407aad42";
Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC6265, rawCookie);
assertThat("Cookies.length", cookies.length, is(1));
assertCookie("Cookies[0]", cookies[0], "SID", "31d4d96e407aad42", 0, null);
}
/**
* Example from RFC6265
*/
@Test
public void testRFC6265SidLangExample()
{
String rawCookie = "SID=31d4d96e407aad42; lang=en-US";
Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC6265, rawCookie);
assertThat("Cookies.length", cookies.length, is(2));
assertCookie("Cookies[0]", cookies[0], "SID", "31d4d96e407aad42", 0, null);
assertCookie("Cookies[1]", cookies[1], "lang", "en-US", 0, null);
}
/**
* Basic name=value, following RFC6265 rules
*/
@Test
public void testKeyValue()
{
String rawCookie = "key=value";
Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC6265, rawCookie);
assertThat("Cookies.length", cookies.length, is(1));
assertCookie("Cookies[0]", cookies[0], "key", "value", 0, null);
}
/**
* Basic name=value, following RFC6265 rules
*/
@Test
public void testDollarName()
{
String rawCookie = "$key=value";
Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC6265, rawCookie);
assertThat("Cookies.length", cookies.length, is(0));
}
@Test
public void testMultipleCookies()
{
String rawCookie = "testcookie; server.id=abcd; server.detail=cfg";
// The first cookie "testcookie" should be ignored, per RFC6265, as it's missing the "=" sign.
Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC6265, rawCookie);
assertThat("Cookies.length", cookies.length, is(2));
assertCookie("Cookies[0]", cookies[0], "server.id", "abcd", 0, null);
assertCookie("Cookies[1]", cookies[1], "server.detail", "cfg", 0, null);
}
@Test
public void testExcessiveSemicolons()
{
char[] excessive = new char[65535];
Arrays.fill(excessive, ';');
String rawCookie = "foo=bar; " + new String(excessive) + "; xyz=pdq";
Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC6265, rawCookie);
assertThat("Cookies.length", cookies.length, is(2));
assertCookie("Cookies[0]", cookies[0], "foo", "bar", 0, null);
assertCookie("Cookies[1]", cookies[1], "xyz", "pdq", 0, null);
}
@Test
public void testRFC2965QuotedEscape()
{
String rawCookie = "A=\"double\\\"quote\"";
Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC2965_LEGACY, rawCookie);
assertThat("Cookies.length", cookies.length, is(1));
assertCookie("Cookies[0]", cookies[0], "A", "double\"quote", 0, null);
}
@Test
public void testRFC2965QuotedSpecial()
{
String rawCookie = "A=\", ;\"";
Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC2965_LEGACY, rawCookie);
assertThat("Cookies.length", cookies.length, is(1));
assertCookie("Cookies[0]", cookies[0], "A", ", ;", 0, null);
}
// TODO:
// $X; N=V
// $X=Y; N=V
public static List<Param> parameters()
{
return Arrays.asList(
new Param("A=1; B=2; C=3", "A=1", "B=2", "C=3"),
new Param("A=\"1\"; B=2; C=3", "A=1", "B=2", "C=3"),
new Param("A=1 ; B=2; C=3", "A=1", "B=2", "C=3"),
new Param("A=\"1; B=2\"; C=3", "C=3"),
new Param("A=\"1; B=2; C=3"),
new Param("A=\"1 B=2\"; C=3", "C=3"),
new Param("A=\"\"1; B=2; C=3", "B=2", "C=3"),
new Param("A=\"\" ; B=2; C=3", "A=", "B=2", "C=3"),
new Param("A=1\"\"; B=2; C=3", "B=2", "C=3"),
new Param("A=1\"; B=2; C=3", "B=2", "C=3"),
new Param("A=1\"1; B=2; C=3", "B=2", "C=3"),
new Param("A= 1; B=2; C=3", "A=1", "B=2", "C=3"),
new Param("A=\" 1\"; B=2; C=3", "B=2", "C=3"),
new Param("A=\"1 \"; B=2; C=3", "B=2", "C=3"),
new Param("A=1,; B=2; C=3", "B=2", "C=3"),
new Param("A=\"1,\"; B=2; C=3", "B=2", "C=3"),
new Param("A=\\1; B=2; C=3", "B=2", "C=3"),
new Param("A=\"\\1\"; B=2; C=3", "B=2", "C=3"),
new Param("A=1\u0007; B=2; C=3", "B=2", "C=3"),
new Param("A=\"1\u0007\"; B=2; C=3", "B=2", "C=3"),
new Param(""),
new Param("@={}"),
new Param("$X=Y; N=V", "N=V"),
new Param("N=V; $X=Y", "N=V")
);
}
@ParameterizedTest
@MethodSource("parameters")
public void testRFC6265Cookie(Param param)
{
Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC6265, param.input);
assertThat("Cookies.length", cookies.length, is(param.expected.size()));
for (int i = 0; i < cookies.length; i++)
{
Cookie cookie = cookies[i];
assertThat(cookie.getName() + "=" + cookie.getValue(), is(param.expected.get(i)));
}
}
static class Cookie
{
String name;
String value;
String domain;
String path;
int version;
String comment;
public Cookie(String name, String value, String domain, String path, int version, String comment)
{
this.name = name;
this.value = value;
this.domain = domain;
this.path = path;
this.version = version;
this.comment = comment;
}
public String getName()
{
return name;
}
public String getValue()
{
return value;
}
public String getDomain()
{
return domain;
}
public String getPath()
{
return path;
}
public int getVersion()
{
return version;
}
public String getComment()
{
return comment;
}
}
private Cookie[] parseCookieHeaders(CookieCompliance compliance, String... headers)
{
TestCookieParser cutter = new TestCookieParser(compliance);
for (String header : headers)
{
cutter.parseFields(header);
}
return cutter.cookies.toArray(Cookie[]::new);
}
private void assertCookie(String prefix, Cookie cookie,
String expectedName,
String expectedValue,
int expectedVersion,
String expectedPath)
{
assertThat(prefix + ".name", cookie.getName(), is(expectedName));
assertThat(prefix + ".value", cookie.getValue(), is(expectedValue));
assertThat(prefix + ".version", cookie.getVersion(), is(expectedVersion));
assertThat(prefix + ".path", cookie.getPath(), is(expectedPath));
}
private static class TestCookieParser implements ComplianceViolation.Listener, CookieParser.Handler
{
private final CookieParser parser;
private final List<Cookie> cookies = new ArrayList<>();
private final List<CookieCompliance.Violation> violations = new ArrayList<>();
private TestCookieParser(CookieCompliance compliance)
{
parser = new RFC6265CookieParser(this, compliance, this);
}
@Override
public void onComplianceViolation(ComplianceViolation.Mode mode, ComplianceViolation violation, String details)
{
violations.add((CookieCompliance.Violation)violation);
}
private List<Cookie> parseFields(String... fields)
{
parser.parseFields(Arrays.asList(fields));
return cookies;
}
@Override
public void addCookie(String cookieName, String cookieValue, int cookieVersion, String cookieDomain, String cookiePath, String cookieComment)
{
cookies.add(new Cookie(cookieName, cookieValue, cookieDomain, cookiePath, cookieVersion, cookieComment));
}
}
private static class Param
{
private final String input;
private final List<String> expected;
public Param(String input, String... expected)
{
this.input = input;
this.expected = Arrays.asList(expected);
}
@Override
public String toString()
{
return input + " -> " + expected.toString();
}
}
}

View File

@ -19,7 +19,7 @@ import javax.servlet.http.Cookie;
import org.eclipse.jetty.http.ComplianceViolation;
import org.eclipse.jetty.http.CookieCompliance;
import org.eclipse.jetty.http.CookieCutter;
import org.eclipse.jetty.http.CookieParser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -31,7 +31,7 @@ import org.slf4j.LoggerFactory;
* If the added fields are identical to those last added (as strings), then the
* cookies are not re parsed.
*/
public class Cookies extends CookieCutter
public class Cookies implements CookieParser.Handler
{
protected static final Logger LOG = LoggerFactory.getLogger(Cookies.class);
protected final List<String> _rawFields = new ArrayList<>();
@ -41,6 +41,8 @@ public class Cookies extends CookieCutter
private Cookie[] _cookies;
private boolean _set = false;
private final CookieParser _parser;
public Cookies()
{
this(CookieCompliance.RFC6265, null);
@ -48,7 +50,7 @@ public class Cookies extends CookieCutter
public Cookies(CookieCompliance compliance, ComplianceViolation.Listener complianceListener)
{
super(compliance, complianceListener);
_parser = CookieParser.newParser(this, compliance, complianceListener);
}
public void addCookieField(String rawField)
@ -93,7 +95,7 @@ public class Cookies extends CookieCutter
if (_parsed)
return _cookies;
parseFields(_rawFields);
_parser.parseFields(_rawFields);
_cookies = (Cookie[])_cookieList.toArray(new Cookie[_cookieList.size()]);
_cookieList.clear();
_parsed = true;
@ -115,7 +117,7 @@ public class Cookies extends CookieCutter
}
@Override
protected void addCookie(String name, String value, String domain, String path, int version, String comment)
public void addCookie(String name, String value, int version, String domain, String path, String comment)
{
try
{

View File

@ -154,6 +154,7 @@ public class RequestTest
http.getHttpConfiguration().setResponseHeaderSize(512);
http.getHttpConfiguration().setOutputBufferSize(2048);
http.getHttpConfiguration().addCustomizer(new ForwardedRequestCustomizer());
http.getHttpConfiguration().setRequestCookieCompliance(CookieCompliance.RFC6265_LEGACY);
_connector = new LocalConnector(_server, http);
_server.addConnector(_connector);
_connector.setIdleTimeout(500);