There are 3 HTTP cookie specifications: *
* Netscape draft* *
* RFC 2109 - * http://www.ietf.org/rfc/rfc2109.txt
* RFC 2965 - * http://www.ietf.org/rfc/rfc2965.txt *
HttpCookie class can accept all these 3 forms of syntax. * * @author Edward Wang * @since 1.6 */ public final class HttpCookie implements Cloneable { // ---------------- Fields -------------- // The value of the cookie itself. private final String name; // NAME= ... "$Name" style is reserved private String value; // value of NAME // Attributes encoded in the header's cookie fields. private String comment; // Comment=VALUE ... describes cookie's use private String commentURL; // CommentURL="http URL" ... describes cookie's use private boolean toDiscard; // Discard ... discard cookie unconditionally private String domain; // Domain=VALUE ... domain that sees cookie private long maxAge = MAX_AGE_UNSPECIFIED; // Max-Age=VALUE ... cookies auto-expire private String path; // Path=VALUE ... URLs that see the cookie private String portlist; // Port[="portlist"] ... the port cookie may be returned to private boolean secure; // Secure ... e.g. use SSL private boolean httpOnly; // HttpOnly ... i.e. not accessible to scripts private int version = 1; // Version=1 ... RFC 2965 style // The original header this cookie was consructed from, if it was // constructed by parsing a header, otherwise null. private final String header; // Hold the creation time (in seconds) of the http cookie for later // expiration calculation private final long whenCreated; // Since the positive and zero max-age have their meanings, // this value serves as a hint as 'not specify max-age' private final static long MAX_AGE_UNSPECIFIED = -1; // date formats used by Netscape's cookie draft // as well as formats seen on various sites private final static String[] COOKIE_DATE_FORMATS = { "EEE',' dd-MMM-yyyy HH:mm:ss 'GMT'", "EEE',' dd MMM yyyy HH:mm:ss 'GMT'", "EEE MMM dd yyyy HH:mm:ss 'GMT'Z", "EEE',' dd-MMM-yy HH:mm:ss 'GMT'", "EEE',' dd MMM yy HH:mm:ss 'GMT'", "EEE MMM dd yy HH:mm:ss 'GMT'Z" }; // constant strings represent set-cookie header token private final static String SET_COOKIE = "set-cookie:"; private final static String SET_COOKIE2 = "set-cookie2:"; // ---------------- Ctors -------------- /** * Constructs a cookie with a specified name and value. * *
The name must conform to RFC 2965. That means it can contain * only ASCII alphanumeric characters and cannot contain commas, * semicolons, or white space or begin with a $ character. The cookie's * name cannot be changed after creation. * *
The value can be anything the server chooses to send. Its * value is probably of interest only to the server. The cookie's * value can be changed after creation with the * {@code setValue} method. * *
By default, cookies are created according to the RFC 2965
* cookie specification. The version can be changed with the
* {@code setVersion} method.
*
*
* @param name
* a {@code String} specifying the name of the cookie
*
* @param value
* a {@code String} specifying the value of the cookie
*
* @throws IllegalArgumentException
* if the cookie name contains illegal characters
* @throws NullPointerException
* if {@code name} is {@code null}
*
* @see #setValue
* @see #setVersion
*/
public HttpCookie(String name, String value) {
this(name, value, null /*header*/);
}
private HttpCookie(String name, String value, String header) {
name = name.trim();
if (name.length() == 0 || !isToken(name) || name.charAt(0) == '$') {
throw new IllegalArgumentException("Illegal cookie name");
}
this.name = name;
this.value = value;
toDiscard = false;
secure = false;
whenCreated = System.currentTimeMillis();
portlist = null;
this.header = header;
}
/**
* Constructs cookies from set-cookie or set-cookie2 header string.
* RFC 2965 section 3.2.2 set-cookie2 syntax indicates that one header line
* may contain more than one cookie definitions, so this is a static
* utility method instead of another constructor.
*
* @param header
* a {@code String} specifying the set-cookie header. The header
* should start with "set-cookie", or "set-cookie2" token; or it
* should have no leading token at all.
*
* @return a List of cookie parsed from header line string
*
* @throws IllegalArgumentException
* if header string violates the cookie specification's syntax or
* the cookie name contains illegal characters.
* @throws NullPointerException
* if the header string is {@code null}
*/
public static List
The form of the domain name is specified by RFC 2965. A domain * name begins with a dot ({@code .foo.com}) and means that * the cookie is visible to servers in a specified Domain Name System * (DNS) zone (for example, {@code www.foo.com}, but not * {@code a.b.foo.com}). By default, cookies are only returned * to the server that sent them. * * @param pattern * a {@code String} containing the domain name within which this * cookie is visible; form is according to RFC 2965 * * @see #getDomain */ public void setDomain(String pattern) { if (pattern != null) domain = pattern.toLowerCase(); else domain = pattern; } /** * Returns the domain name set for this cookie. The form of the domain name * is set by RFC 2965. * * @return a {@code String} containing the domain name * * @see #setDomain */ public String getDomain() { return domain; } /** * Sets the maximum age of the cookie in seconds. * *
A positive value indicates that the cookie will expire * after that many seconds have passed. Note that the value is * the maximum age when the cookie will expire, not the cookie's * current age. * *
A negative value means that the cookie is not stored persistently * and will be deleted when the Web browser exits. A zero value causes the * cookie to be deleted. * * @param expiry * an integer specifying the maximum age of the cookie in seconds; * if zero, the cookie should be discarded immediately; otherwise, * the cookie's max age is unspecified. * * @see #getMaxAge */ public void setMaxAge(long expiry) { maxAge = expiry; } /** * Returns the maximum age of the cookie, specified in seconds. By default, * {@code -1} indicating the cookie will persist until browser shutdown. * * @return an integer specifying the maximum age of the cookie in seconds * * @see #setMaxAge */ public long getMaxAge() { return maxAge; } /** * Specifies a path for the cookie to which the client should return * the cookie. * *
The cookie is visible to all the pages in the directory * you specify, and all the pages in that directory's subdirectories. * A cookie's path must include the servlet that set the cookie, * for example, /catalog, which makes the cookie * visible to all directories on the server under /catalog. * *
Consult RFC 2965 (available on the Internet) for more * information on setting path names for cookies. * * @param uri * a {@code String} specifying a path * * @see #getPath */ public void setPath(String uri) { path = uri; } /** * Returns the path on the server to which the browser returns this cookie. * The cookie is visible to all subpaths on the server. * * @return a {@code String} specifying a path that contains a servlet name, * for example, /catalog * * @see #setPath */ public String getPath() { return path; } /** * Indicates whether the cookie should only be sent using a secure protocol, * such as HTTPS or SSL. * *
The default value is {@code false}. * * @param flag * If {@code true}, the cookie can only be sent over a secure * protocol like HTTPS. If {@code false}, it can be sent over * any protocol. * * @see #getSecure */ public void setSecure(boolean flag) { secure = flag; } /** * Returns {@code true} if sending this cookie should be restricted to a * secure protocol, or {@code false} if the it can be sent using any * protocol. * * @return {@code false} if the cookie can be sent over any standard * protocol; otherwise, {@code true} * * @see #setSecure */ public boolean getSecure() { return secure; } /** * Returns the name of the cookie. The name cannot be changed after * creation. * * @return a {@code String} specifying the cookie's name */ public String getName() { return name; } /** * Assigns a new value to a cookie after the cookie is created. * If you use a binary value, you may want to use BASE64 encoding. * *
With Version 0 cookies, values should not contain white space, * brackets, parentheses, equals signs, commas, double quotes, slashes, * question marks, at signs, colons, and semicolons. Empty values may not * behave the same way on all browsers. * * @param newValue * a {@code String} specifying the new value * * @see #getValue */ public void setValue(String newValue) { value = newValue; } /** * Returns the value of the cookie. * * @return a {@code String} containing the cookie's present value * * @see #setValue */ public String getValue() { return value; } /** * Returns the version of the protocol this cookie complies with. Version 1 * complies with RFC 2965/2109, and version 0 complies with the original * cookie specification drafted by Netscape. Cookies provided by a browser * use and identify the browser's cookie version. * * @return 0 if the cookie complies with the original Netscape * specification; 1 if the cookie complies with RFC 2965/2109 * * @see #setVersion */ public int getVersion() { return version; } /** * Sets the version of the cookie protocol this cookie complies * with. Version 0 complies with the original Netscape cookie * specification. Version 1 complies with RFC 2965/2109. * * @param v * 0 if the cookie should comply with the original Netscape * specification; 1 if the cookie should comply with RFC 2965/2109 * * @throws IllegalArgumentException * if {@code v} is neither 0 nor 1 * * @see #getVersion */ public void setVersion(int v) { if (v != 0 && v != 1) { throw new IllegalArgumentException("cookie version should be 0 or 1"); } version = v; } /** * Returns {@code true} if this cookie contains the HttpOnly * attribute. This means that the cookie should not be accessible to * scripting engines, like javascript. * * @return {@code true} if this cookie should be considered HTTPOnly * * @see #setHttpOnly(boolean) */ public boolean isHttpOnly() { return httpOnly; } /** * Indicates whether the cookie should be considered HTTP Only. If set to * {@code true} it means the cookie should not be accessible to scripting * engines like javascript. * * @param httpOnly * if {@code true} make the cookie HTTP only, i.e. only visible as * part of an HTTP request. * * @see #isHttpOnly() */ public void setHttpOnly(boolean httpOnly) { this.httpOnly = httpOnly; } /** * The utility method to check whether a host name is in a domain or not. * *
This concept is described in the cookie specification. * To understand the concept, some terminologies need to be defined first: *
* effective host name = hostname if host name contains dot*
* * or = hostname.local if not *
Host A's name domain-matches host B's if: *
* **
- their host name strings string-compare equal; or
*- A is a HDN string and has the form NB, where N is a non-empty * name string, B has the form .B', and B' is a HDN string. (So, * x.y.com domain-matches .Y.com but not Y.com.)
*
A host isn't in a domain (RFC 2965 sec. 3.3.2) if: *
* **
- The value for the Domain attribute contains no embedded dots, * and the value is not .local.
*- The effective host name that derives from the request-host does * not domain-match the Domain attribute.
*- The request-host is a HDN (not IP address) and has the form HD, * where D is the value of the Domain attribute, and H is a string * that contains one or more dots.
*
Examples: *
* * @param domain * the domain name to check host name with * * @param host * the host name in question * * @return {@code true} if they domain-matches; {@code false} if not */ public static boolean domainMatches(String domain, String host) { if (domain == null || host == null) return false; // if there's no embedded dot in domain and domain is not .local boolean isLocalDomain = ".local".equalsIgnoreCase(domain); int embeddedDotInDomain = domain.indexOf('.'); if (embeddedDotInDomain == 0) embeddedDotInDomain = domain.indexOf('.', 1); if (!isLocalDomain && (embeddedDotInDomain == -1 || embeddedDotInDomain == domain.length() - 1)) return false; // if the host name contains no dot and the domain name // is .local or host.local int firstDotInHost = host.indexOf('.'); if (firstDotInHost == -1 && (isLocalDomain || domain.equalsIgnoreCase(host + ".local"))) { return true; } int domainLength = domain.length(); int lengthDiff = host.length() - domainLength; if (lengthDiff == 0) { // if the host name and the domain name are just string-compare euqal return host.equalsIgnoreCase(domain); } else if (lengthDiff > 0) { // need to check H & D component String H = host.substring(0, lengthDiff); String D = host.substring(lengthDiff); return (H.indexOf('.') == -1 && D.equalsIgnoreCase(domain)); } else if (lengthDiff == -1) { // if domain is actually .host return (domain.charAt(0) == '.' && host.equalsIgnoreCase(domain.substring(1))); } return false; } /** * Constructs a cookie header string representation of this cookie, * which is in the format defined by corresponding cookie specification, * but without the leading "Cookie:" token. * * @return a string form of the cookie. The string has the defined format */ @Override public String toString() { if (getVersion() > 0) { return toRFC2965HeaderString(); } else { return toNetscapeHeaderString(); } } /** * Test the equality of two HTTP cookies. * **
- A Set-Cookie2 from request-host y.x.foo.com for Domain=.foo.com * would be rejected, because H is y.x and contains a dot.
*- A Set-Cookie2 from request-host x.foo.com for Domain=.foo.com * would be accepted.
*- A Set-Cookie2 with Domain=.com or Domain=.com., will always be * rejected, because there is no embedded dot.
*- A Set-Cookie2 from request-host example for Domain=.local will * be accepted, because the effective host name for the request- * host is example.local, and example.local domain-matches .local.
*
The result is {@code true} only if two cookies come from same domain * (case-insensitive), have same name (case-insensitive), and have same path * (case-sensitive). * * @return {@code true} if two HTTP cookies equal to each other; * otherwise, {@code false} */ @Override public boolean equals(Object obj) { if (obj == this) return true; if (!(obj instanceof HttpCookie)) return false; HttpCookie other = (HttpCookie)obj; // One http cookie equals to another cookie (RFC 2965 sec. 3.3.3) if: // 1. they come from same domain (case-insensitive), // 2. have same name (case-insensitive), // 3. and have same path (case-sensitive). return equalsIgnoreCase(getName(), other.getName()) && equalsIgnoreCase(getDomain(), other.getDomain()) && Objects.equals(getPath(), other.getPath()); } /** * Returns the hash code of this HTTP cookie. The result is the sum of * hash code value of three significant components of this cookie: name, * domain, and path. That is, the hash code is the value of the expression: *
* getName().toLowerCase().hashCode()* * @return this HTTP cookie's hash code */ @Override public int hashCode() { int h1 = name.toLowerCase().hashCode(); int h2 = (domain!=null) ? domain.toLowerCase().hashCode() : 0; int h3 = (path!=null) ? path.hashCode() : 0; return h1 + h2 + h3; } /** * Create and return a copy of this object. * * @return a clone of this HTTP cookie */ @Override public Object clone() { try { return super.clone(); } catch (CloneNotSupportedException e) { throw new RuntimeException(e.getMessage()); } } // ---------------- Private operations -------------- // Note -- disabled for now to allow full Netscape compatibility // from RFC 2068, token special case characters // // private static final String tspecials = "()<>@,;:\\\"/[]?={} \t"; private static final String tspecials = ",; "; // deliberately includes space /* * Tests a string and returns true if the string counts as a token. * * @param value * the {@code String} to be tested * * @return {@code true} if the {@code String} is a token; * {@code false} if it is not */ private static boolean isToken(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; } /* * Parse header string to cookie object. * * @param header * header string; should contain only one NAME=VALUE pair * * @return an HttpCookie being extracted * * @throws IllegalArgumentException * if header string violates the cookie specification */ private static HttpCookie parseInternal(String header, boolean retainHeader) { HttpCookie cookie = null; String namevaluePair = null; StringTokenizer tokenizer = new StringTokenizer(header, ";"); // there should always have at least on name-value pair; // it's cookie's name try { namevaluePair = tokenizer.nextToken(); int index = namevaluePair.indexOf('='); if (index != -1) { String name = namevaluePair.substring(0, index).trim(); String value = namevaluePair.substring(index + 1).trim(); if (retainHeader) cookie = new HttpCookie(name, stripOffSurroundingQuote(value), header); else cookie = new HttpCookie(name, stripOffSurroundingQuote(value)); } else { // no "=" in name-value pair; it's an error throw new IllegalArgumentException("Invalid cookie name-value pair"); } } catch (NoSuchElementException ignored) { throw new IllegalArgumentException("Empty cookie header string"); } // remaining name-value pairs are cookie's attributes while (tokenizer.hasMoreTokens()) { namevaluePair = tokenizer.nextToken(); int index = namevaluePair.indexOf('='); String name, value; if (index != -1) { name = namevaluePair.substring(0, index).trim(); value = namevaluePair.substring(index + 1).trim(); } else { name = namevaluePair.trim(); value = null; } // assign attribute to cookie assignAttribute(cookie, name, value); } return cookie; } /* * assign cookie attribute value to attribute name; * use a map to simulate method dispatch */ static interface CookieAttributeAssignor { public void assign(HttpCookie cookie, String attrName, String attrValue); } static final java.util.Map
* + getDomain().toLowerCase().hashCode()
* + getPath().hashCode() *