Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Servlet api issue 175 add cookie set attribute v3 #401

Merged
Merged
198 changes: 149 additions & 49 deletions api/src/main/java/jakarta/servlet/http/Cookie.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,12 @@
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.text.MessageFormat;
import java.util.Collections;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.ResourceBundle;
import java.util.TreeMap;

/**
*
Expand All @@ -44,7 +48,7 @@
* <p>
* The browser returns cookies to the servlet by adding fields to HTTP request headers. Cookies can be retrieved from a
* request by using the {@link HttpServletRequest#getCookies} method. Several cookies might have the same name but
* different path attributes.
* different path attributes().
*
gregw marked this conversation as resolved.
Show resolved Hide resolved
* <p>
* Cookies affect the caching of the Web pages that use them. HTTP 1.0 does not cache pages that use cookies created
Expand All @@ -58,13 +62,20 @@
*/
public class Cookie implements Cloneable, Serializable {

private static final long serialVersionUID = -6454587001725327448L;
private static final long serialVersionUID = -5433071011125749022L;

private static final String TSPECIALS;

private static final String LSTRING_FILE = "jakarta.servlet.http.LocalStrings";

private static ResourceBundle lStrings = ResourceBundle.getBundle(LSTRING_FILE);
private static final String COMMENT = "Comment"; // ;Comment=VALUE ... describes cookie's use
private static final String DOMAIN = "Domain"; // ;Domain=VALUE ... domain that sees cookie
private static final String MAX_AGE = "Max-Age"; // ;Max-Age=VALUE ... cookies auto-expire
private static final String PATH = "Path"; // ;Path=VALUE ... URLs that see the cookie
private static final String SECURE = "Secure"; // ;Secure ... e.g. use SSL
private static final String HTTP_ONLY = "HttpOnly";

private static final ResourceBundle lStrings = ResourceBundle.getBundle(LSTRING_FILE);

static {
boolean enforced = AccessController.doPrivileged(new PrivilegedAction<Boolean>() {
Expand All @@ -83,22 +94,20 @@ public Boolean run() {
//
// The value of the cookie itself.
//

private String name; // NAME= ... "$Name" style is reserved
private final String name; // NAME= ... "$Name" style is reserved
private String value; // value of NAME
gregw marked this conversation as resolved.
Show resolved Hide resolved
private int version = 0; // ;Version=1 ... means RFC 2109++ style

//
// Attributes encoded in the header's cookie fields.
//
private Map<String, String> attributes = null;

private String comment; // ;Comment=VALUE ... describes cookie's use
// ;Discard ... implied by maxAge < 0
private String domain; // ;Domain=VALUE ... domain that sees cookie
private int maxAge = -1; // ;Max-Age=VALUE ... cookies auto-expire
private String path; // ;Path=VALUE ... URLs that see the cookie
private boolean secure; // ;Secure ... e.g. use SSL
private int version = 0; // ;Version=1 ... means RFC 2109++ style
private boolean isHttpOnly = false;
private void putAttribute(String name, String value) {
if (attributes == null)
attributes = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
attributes.put(name, value);
}
gregw marked this conversation as resolved.
Show resolved Hide resolved

/**
* Constructs a cookie with the specified name and value.
Expand Down Expand Up @@ -129,20 +138,21 @@ public Boolean run() {
* @see #setVersion
*/
public Cookie(String name, String value) {
if (name == null || name.length() == 0) {
throw new IllegalArgumentException(lStrings.getString("err.cookie_name_blank"));
if (name == null || name.isEmpty()) {
throw new IllegalArgumentException(createErrorMessage("err.cookie_name_blank"));
}
if (!isToken(name) || name.equalsIgnoreCase("Comment") || // rfc2019
name.equalsIgnoreCase("Discard") || // 2019++
name.equalsIgnoreCase("Domain") || name.equalsIgnoreCase("Expires") || // (old cookies)
name.equalsIgnoreCase("Max-Age") || // rfc2019
name.equalsIgnoreCase("Path") || name.equalsIgnoreCase("Secure") || name.equalsIgnoreCase("Version")
|| name.startsWith("$")) {
String errMsg = lStrings.getString("err.cookie_name_is_token");
Object[] errArgs = new Object[1];
errArgs[0] = name;
errMsg = MessageFormat.format(errMsg, errArgs);
throw new IllegalArgumentException(errMsg);

if (containsReservedToken(name) || name.startsWith("$")
|| name.equalsIgnoreCase(COMMENT) // rfc2109
|| name.equalsIgnoreCase("Discard") // 2109++
|| name.equalsIgnoreCase(DOMAIN)
|| name.equalsIgnoreCase("Expires") // (old cookies)
|| name.equalsIgnoreCase(MAX_AGE) // rfc2109
|| name.equalsIgnoreCase(PATH)
|| name.equalsIgnoreCase(SECURE)
|| name.equalsIgnoreCase("Version")
|| name.equalsIgnoreCase(HTTP_ONLY)) {
throw new IllegalArgumentException(createErrorMessage("err.cookie_name_is_token", name));
}

this.name = name;
Expand All @@ -158,7 +168,7 @@ public Cookie(String name, String value) {
* @see #getComment
*/
public void setComment(String purpose) {
comment = purpose;
putAttribute(COMMENT, purpose);
}

/**
Expand All @@ -169,7 +179,7 @@ public void setComment(String purpose) {
* @see #setComment
*/
public String getComment() {
return comment;
return getAttribute(COMMENT);
}

/**
Expand All @@ -187,7 +197,7 @@ public String getComment() {
* @see #getDomain
*/
public void setDomain(String domain) {
this.domain = domain != null ? domain.toLowerCase(Locale.ENGLISH) : null; // IE allegedly needs this
putAttribute(DOMAIN, domain != null ? domain.toLowerCase(Locale.ENGLISH) : null); // IE allegedly needs this
}

/**
Expand All @@ -201,7 +211,7 @@ public void setDomain(String domain) {
* @see #setDomain
*/
public String getDomain() {
return domain;
return getAttribute(DOMAIN);
}

/**
Expand All @@ -221,7 +231,7 @@ public String getDomain() {
* @see #getMaxAge
*/
public void setMaxAge(int expiry) {
maxAge = expiry;
putAttribute(MAX_AGE, String.valueOf(expiry));
}

/**
Expand All @@ -234,9 +244,13 @@ public void setMaxAge(int expiry) {
* browser shutdown
*
* @see #setMaxAge
*
* @throws NumberFormatException when this attribute is set via {@link #setAttribute(String, String)} with a value which
* is not in number format
gregw marked this conversation as resolved.
Show resolved Hide resolved
*/
public int getMaxAge() {
return maxAge;
String maxAge = getAttribute(MAX_AGE);
return maxAge != null ? Integer.parseInt(maxAge) : -1;
gregw marked this conversation as resolved.
Show resolved Hide resolved
}

/**
Expand All @@ -256,7 +270,7 @@ public int getMaxAge() {
* @see #getPath
*/
public void setPath(String uri) {
path = uri;
putAttribute(PATH, uri);
}

/**
Expand All @@ -268,7 +282,7 @@ public void setPath(String uri) {
* @see #setPath
*/
public String getPath() {
return path;
return getAttribute(PATH);
}

/**
Expand All @@ -283,7 +297,7 @@ public String getPath() {
* @see #getSecure
*/
public void setSecure(boolean flag) {
secure = flag;
putAttribute(SECURE, String.valueOf(flag));
}

/**
Expand All @@ -295,7 +309,7 @@ public void setSecure(boolean flag) {
* @see #setSecure
*/
public boolean getSecure() {
return secure;
return Boolean.parseBoolean(getAttribute(SECURE));
}

/**
Expand Down Expand Up @@ -356,9 +370,6 @@ public int getVersion() {
* <p>
* Version 0 complies with the original Netscape cookie specification. Version 1 complies with RFC 2109.
*
* <p>
* Since RFC 2109 is still somewhat new, consider version 1 as experimental; do not use it yet on production sites.
*
* @param v 0 if the cookie should comply with the original Netscape specification; 1 if the cookie should comply with
* RFC 2109
*
Expand All @@ -369,22 +380,31 @@ public void setVersion(int v) {
}

/*
* Tests a string and returns true if the string counts as a reserved token in the Java language.
* Tests a string and returns true if the string contains a reserved token for the Set-Cookie header.
*
gregw marked this conversation as resolved.
Show resolved Hide resolved
* @param value the <code>String</code> to be tested
*
* @return <code>true</code> if the <code>String</code> is a reserved token; <code>false</code> otherwise
* @return <code>true</code> if the <code>String</code> contains a reserved token for the Set-Cookie header;
* <code>false</code> otherwise
*/
private boolean isToken(String value) {
private static boolean containsReservedToken(String value) {
int len = value.length();
for (int i = 0; i < len; i++) {
char c = value.charAt(i);
if (c < 0x20 || c >= 0x7f || TSPECIALS.indexOf(c) != -1) {
return false;
return true;
}
}

return true;
return false;
}

/*
* Create error message to be set as exception detail message.
*/
private static String createErrorMessage(String key, Object... arguments) {
String errMsg = lStrings.getString(key);
return MessageFormat.format(errMsg, arguments);
}

/**
Expand All @@ -403,19 +423,19 @@ public Object clone() {
* Marks or unmarks this Cookie as <i>HttpOnly</i>.
*
* <p>
* If <tt>isHttpOnly</tt> is set to <tt>true</tt>, this cookie is marked as <i>HttpOnly</i>, by adding the
* If <tt>httpOnly</tt> is set to <tt>true</tt>, this cookie is marked as <i>HttpOnly</i>, by adding the
* <tt>HttpOnly</tt> attribute to it.
*
* <p>
* <i>HttpOnly</i> cookies are not supposed to be exposed to client-side scripting code, and may therefore help mitigate
* certain kinds of cross-site scripting attacks.
*
* @param isHttpOnly true if this cookie is to be marked as <i>HttpOnly</i>, false otherwise
* @param httpOnly true if this cookie is to be marked as <i>HttpOnly</i>, false otherwise
*
* @since Servlet 3.0
*/
public void setHttpOnly(boolean isHttpOnly) {
this.isHttpOnly = isHttpOnly;
public void setHttpOnly(boolean httpOnly) {
putAttribute(HTTP_ONLY, String.valueOf(httpOnly));
}

/**
Expand All @@ -426,6 +446,86 @@ public void setHttpOnly(boolean isHttpOnly) {
* @since Servlet 3.0
*/
public boolean isHttpOnly() {
return isHttpOnly;
return Boolean.parseBoolean(getAttribute(HTTP_ONLY));
}

/**
* Sets the value of the cookie attribute associated with the given name.
*
* <p>
* This should sync to any predefined attribute for which already a getter/setter pair exist in the current version,
* except for <code>version</code>. E.g. when <code>cookie.setAttribute("domain", domain)</code> is invoked, then
* <code>cookie.getDomain()</code> should return exactly that value, and vice versa.
*
* @param name the name of the cookie attribute to set the value for, case insensitive
*
* @param value the value of the cookie attribute associated with the given name, can be {@code null}
*
* @throws IllegalArgumentException if the cookie name is null or empty or contains any illegal characters (for example,
* a comma, space, or semicolon) or matches a token reserved for use by the cookie protocol
*
* @since Servlet 5.1
*/
public void setAttribute(String name, String value) {
if (name == null || name.isEmpty()) {
throw new IllegalArgumentException(createErrorMessage("err.cookie_attribute_name_blank"));
}

if (containsReservedToken(name)) {
throw new IllegalArgumentException(createErrorMessage("err.cookie_attribute_name_is_token", name));
}

putAttribute(name, value);
}

/**
* Gets the value of the cookie attribute associated with the given name.
*
* <p>
* This should sync to any predefined attribute for which already a getter/setter pair exist in the current version,
* except for <code>version</code>. E.g. when <code>cookie.setAttribute("domain", domain)</code> is invoked, then
* <code>cookie.getDomain()</code> should return exactly that value, and vice versa.
*
* @param name the name of the cookie attribute to set the value for, case insensitive
*
* @since Servlet 5.1
*/
public String getAttribute(String name) {
return attributes == null ? null : attributes.get(name);
}

/**
* Returns an unmodifiable mapping of all cookie attributes set via {@link #setAttribute(String, String)} as well as any
* predefined setter method, except for <code>version</code>.
*
* @return an unmodifiable mapping of all cookie attributes set via <code>setAttribute(String, String)</code> as well as
* any predefined setter method, except for <code>version</code>
*
* @since Servlet 5.1
*/
public Map<String, String> getAttributes() {
return Collections.unmodifiableMap(attributes == null ? Collections.emptyMap() : attributes);
gregw marked this conversation as resolved.
Show resolved Hide resolved
}

@Override
public int hashCode() {
return Objects.hash(name, value, attributes) + version;
}

@Override
public boolean equals(Object obj) {
if (obj instanceof Cookie) {
Cookie c = (Cookie) obj;
return Objects.equals(getName(), c.getName()) &&
Objects.equals(getValue(), c.getValue()) &&
getVersion() == c.getVersion() &&
Objects.equals(getAttributes(), c.getAttributes());
}
return false;
}

@Override
public String toString() {
return String.format("%s{%s=%s,%d,%s}", super.toString(), name, value, version, attributes);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@
# Default localized string information
# Localized for Locale en_US

err.cookie_name_is_token=Cookie name \"{0}\" is a reserved token
err.cookie_name_is_token=Cookie name \"{0}\" contains a reserved token
err.cookie_name_blank=Cookie name must not be null or empty
gregw marked this conversation as resolved.
Show resolved Hide resolved
err.cookie_attribute_name_is_token=Cookie attribute name \"{0}\" contains a reserved token
gregw marked this conversation as resolved.
Show resolved Hide resolved
err.cookie_attribute_name_blank=Cookie attribute name must not be null or empty
err.io.nullArray=Null passed for byte array in write method
err.io.indexOutOfBounds=Invalid offset [{0}] and / or length [{1}] specified for array of size [{2}]
err.io.short_read=Short Read
Expand Down