HTTPClient.Cookie Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of grinder-httpclient Show documentation
Show all versions of grinder-httpclient Show documentation
Modified version of HTTPClient used by The Grinder. The original
can be found at http://www.innovation.ch/java/HTTPClient/.
/*
* @(#)Cookie.java 0.3-3 06/05/2001
*
* This file is part of the HTTPClient package
* Copyright (C) 1996-2001 Ronald Tschalär
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free
* Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
* MA 02111-1307, USA
*
* For questions, suggestions, bug-reports, enhancement-requests etc.
* I may be contacted at:
*
* [email protected]
*
* The HTTPClient's home page is located at:
*
* http://www.innovation.ch/java/HTTPClient/
*
* This file contains modifications for use with "The Grinder"
* (http://grinder.sourceforge.net) under the terms of the LGPL. They
* are marked below with the comment "GRINDER MODIFICATION".
*/
package HTTPClient;
import java.io.Serializable;
import java.net.ProtocolException;
import java.util.Date;
/**
* This class represents an http cookie as specified in Netscape's
* cookie spec; however, because not even Netscape follows their own spec,
* and because very few folks out there actually read specs but instead just
* look whether Netscape accepts their stuff, the Set-Cookie header field
* parser actually tries to follow what Netscape has implemented, instead of
* what the spec says. Additionally, the parser it will also recognize the
* Max-Age parameter from rfc-2109, as that uses the
* same header field (Set-Cookie).
*
* Some notes about how Netscape (4.7) parses:
*
* - Quoting: only quotes around the expires value are recognized as such;
* quotes around any other value are treated as part of the value.
*
- White space: white space around names and values is ignored
*
- Default path: if no path parameter is given, the path defaults to the
* path in the request-uri up to, but not including, the last '/'. Note
* that this is entirely different from what the spec says.
*
- Commas and other delimiters: Netscape just parses until the next ';'.
* This means will allow commas etc inside values.
*
*
* @version 0.3-3 06/05/2001
* @author Ronald Tschalär
* @since V0.3
*/
public class Cookie implements Serializable
{
/** Make this compatible with V0.3-2 */
private static final long serialVersionUID = 8599975325569296615L;
protected String name;
protected String value;
protected Date expires;
protected String domain;
protected String path;
protected boolean secure;
/**
* Create a cookie.
*
* @param name the cookie name
* @param value the cookie value
* @param domain the host this cookie will be sent to
* @param path the path prefix for which this cookie will be sent
* @param epxires the Date this cookie expires, null if at end of
* session
* @param secure if true this cookie will only be over secure connections
* @exception NullPointerException if name, value,
* domain, or path
* is null
* @since V0.3-1
*/
public Cookie(String name, String value, String domain, String path,
Date expires, boolean secure)
{
if (name == null) throw new NullPointerException("missing name");
if (value == null) throw new NullPointerException("missing value");
if (domain == null) throw new NullPointerException("missing domain");
if (path == null) throw new NullPointerException("missing path");
this.name = name;
this.value = value;
this.domain = domain.toLowerCase();
this.path = path;
this.expires = expires;
this.secure = secure;
if (this.domain.indexOf('.') == -1) this.domain += ".local";
}
/**
* Use parse()
to create cookies.
*
* @see #parse(java.lang.String, HTTPClient.RoRequest)
*/
protected Cookie(RoRequest req)
{
name = null;
value = null;
expires = null;
domain = req.getConnection().getHost();
if (domain.indexOf('.') == -1) domain += ".local";
path = Util.getPath(req.getRequestURI());
/* This does not follow netscape's spec at all, but it's the way
* netscape seems to do it, and because people rely on that we
* therefore also have to do it...
*/
int slash = path.lastIndexOf('/');
if (slash >= 0)
path = path.substring(0, slash);
secure = false;
}
/**
* Parses the Set-Cookie header into an array of Cookies.
*
* @param set_cookie the Set-Cookie header received from the server
* @param req the request used
* @return an array of Cookies as parsed from the Set-Cookie header
* @exception ProtocolException if an error occurs during parsing
*/
protected static Cookie[] parse(String set_cookie, RoRequest req)
throws ProtocolException
{
int beg = 0,
end = 0,
start = 0;
/** ++GRINDER MODIFICATION **/
// Cope with .NET nonsense.
// See http://www.hanselman.com/blog/HttpOnlyCookiesOnASPNET11.aspx
if (set_cookie.toLowerCase().indexOf("httponly") != -1) {
// Typical section of a .NET cookie:
// ... ; path=/;HttpOnly, language=en-US; path=/;HttpOnly
//
// We remove all instances of "HttpOnly," that follow a semi-colon,
// and all instances of HttpOnly following a semi-colon at the end of
// the cookie. This leaves something that is more conventional
// This shouldn't break any valid cookies.
set_cookie = set_cookie.replaceAll("(?i);\\s*HttpOnly[,;]",";,");
set_cookie = set_cookie.replaceAll("(?i);\\s*HttpOnly$",";");
}
/** --GRINDER MODIFICATION **/
char[] buf = set_cookie.toCharArray();
int len = buf.length;
Cookie cookie_arr[] = new Cookie[0], curr;
cookies: while (true) // get all cookies
{
beg = Util.skipSpace(buf, beg);
if (beg >= len) break; // no more left
if (buf[beg] == ',') // empty header
{
beg++;
continue;
}
curr = new Cookie(req);
start = beg;
// get cookie name and value first
end = set_cookie.indexOf('=', beg);
if (end == -1)
throw new ProtocolException("Bad Set-Cookie header: " +
set_cookie + "\nNo '=' found " +
"for token starting at " +
"position " + beg);
curr.name = set_cookie.substring(beg, end).trim();
beg = Util.skipSpace(buf, end+1);
int comma = set_cookie.indexOf(',', beg);
int semic = set_cookie.indexOf(';', beg);
if (comma == -1 && semic == -1) end = len;
else if (comma == -1) end = semic;
else if (semic == -1) end = comma;
else
{
if (comma > semic)
end = semic;
else
{
// try to handle broken servers which put commas
// into cookie values
int eq = set_cookie.indexOf('=', comma);
if (eq > 0 && eq < semic)
end = set_cookie.lastIndexOf(',', eq);
else
end = semic;
}
}
curr.value = set_cookie.substring(beg, end).trim();
beg = end;
// now parse attributes
boolean legal = true;
parts: while (true) // parse all parts
{
if (beg >= len || buf[beg] == ',') break;
// skip empty fields
if (buf[beg] == ';')
{
beg = Util.skipSpace(buf, beg+1);
continue;
}
// first check for secure, as this is the only one w/o a '='
if ((beg+6 <= len) &&
set_cookie.regionMatches(true, beg, "secure", 0, 6))
{
curr.secure = true;
beg += 6;
beg = Util.skipSpace(buf, beg);
if (beg < len && buf[beg] == ';') // consume ";"
beg = Util.skipSpace(buf, beg+1);
else if (beg < len && buf[beg] != ',')
throw new ProtocolException("Bad Set-Cookie header: " +
set_cookie + "\nExpected " +
"';' or ',' at position " +
beg);
continue;
}
// alright, must now be of the form x=y
end = set_cookie.indexOf('=', beg);
if (end == -1)
throw new ProtocolException("Bad Set-Cookie header: " +
set_cookie + "\nNo '=' found " +
"for token starting at " +
"position " + beg);
String name = set_cookie.substring(beg, end).trim();
beg = Util.skipSpace(buf, end+1);
if (name.equalsIgnoreCase("expires"))
{
/** ++GRINDER MODIFICATION **/
if (beg >= len)
{
// Empty expires attribute at end of Cookie. We're done.
break;
}
/** --GRINDER MODIFICATION **/
/* Netscape ignores quotes around the date, and some twits
* actually send that...
*/
if (set_cookie.charAt(beg) == '\"')
beg = Util.skipSpace(buf, beg+1);
/* cut off the weekday if it is there. This is a little
* tricky because the comma is also used between cookies
* themselves. To make sure we don't inadvertantly
* mistake a date for a weekday we only skip letters.
*/
int pos = beg;
while (pos < len &&
(buf[pos] >= 'a' && buf[pos] <= 'z' ||
buf[pos] >= 'A' && buf[pos] <= 'Z'))
pos++;
pos = Util.skipSpace(buf, pos);
if (pos < len && buf[pos] == ',' && pos > beg)
beg = pos+1;
/** ++GRINDER MODIFICATION **/
// Some other twits put a comma after the date.
// Replace it with a space.
pos = Util.skipSpace(buf, beg);
// Skip past the date.
while (pos < len &&
(Character.isDigit(buf[pos]) || buf[pos] == '-')) {
++pos;
}
if (pos < len && buf[pos] == ',') {
buf[pos] = ' ';
set_cookie = new String(buf);
}
/** --GRINDER MODIFICATION **/
}
comma = set_cookie.indexOf(',', beg);
semic = set_cookie.indexOf(';', beg);
if (comma == -1 && semic == -1) end = len;
else if (comma == -1) end = semic;
else if (semic == -1) end = comma;
else end = Math.min(comma, semic);
String value = set_cookie.substring(beg, end).trim();
legal &= setAttribute(curr, name, value, set_cookie);
beg = end;
if (beg < len && buf[beg] == ';') // consume ";"
beg = Util.skipSpace(buf, beg+1);
}
if (legal)
{
cookie_arr = Util.resizeArray(cookie_arr, cookie_arr.length+1);
cookie_arr[cookie_arr.length-1] = curr;
} else
Log.write(Log.COOKI, "Cooki: Ignoring cookie: " + curr);
}
return cookie_arr;
}
/**
* Set the given attribute, if valid.
*
* @param cookie the cookie on which to set the value
* @param name the name of the attribute
* @param value the value of the attribute
* @param set_cookie the complete Set-Cookie header
* @return true if the attribute is legal; false otherwise
*/
private static boolean setAttribute(Cookie cookie, String name,
String value, String set_cookie)
throws ProtocolException
{
if (name.equalsIgnoreCase("expires"))
{
if (value.charAt(value.length()-1) == '\"')
value = value.substring(0, value.length()-1).trim();
try
// This is too strict...
// { cookie.expires = Util.parseHttpDate(value); }
{ cookie.expires = new Date(value); }
catch (IllegalArgumentException iae)
{
/* More broken servers to deal with... Ignore expires
* if it's invalid
throw new ProtocolException("Bad Set-Cookie header: " +
set_cookie + "\nInvalid date found at " +
"position " + beg);
*/
Log.write(Log.COOKI, "Cooki: Bad Set-Cookie header: " + set_cookie +
"\n Invalid date `" + value + "'");
}
}
else if (name.equals("max-age")) // from rfc-2109
{
if (cookie.expires != null) return true;
if (value.charAt(0) == '\"' && value.charAt(value.length()-1) == '\"')
value = value.substring(1, value.length()-1).trim();
int age;
try
{ age = Integer.parseInt(value); }
catch (NumberFormatException nfe)
{
throw new ProtocolException("Bad Set-Cookie header: " +
set_cookie + "\nMax-Age '" + value +
"' not a number");
}
cookie.expires = new Date(System.currentTimeMillis() + age*1000L);
}
else if (name.equalsIgnoreCase("domain"))
{
// you get everything these days...
if (value.length() == 0)
{
Log.write(Log.COOKI, "Cooki: Bad Set-Cookie header: " + set_cookie +
"\n domain is empty - ignoring domain");
return true;
}
// domains are case insensitive.
value = value.toLowerCase();
// add leading dot, if missing
if (value.length() != 0 && value.charAt(0) != '.' &&
!value.equals(cookie.domain))
value = '.' + value;
// must be the same domain as in the url
if (!cookie.domain.endsWith(value))
{
Log.write(Log.COOKI, "Cooki: Bad Set-Cookie header: " + set_cookie +
"\n Current domain " + cookie.domain +
" does not match given parsed " + value);
return false;
}
/* Netscape's original 2-/3-dot rule really doesn't work because
* many countries use a shallow hierarchy (similar to the special
* TLDs defined in the spec). While the rules in rfc-2965 aren't
* perfect either, they are better. OTOH, some sites use a domain
* so that the host name minus the domain name contains a dot (e.g.
* host x.x.yahoo.com and domain .yahoo.com). So, for the seven
* special TLDs we use the 2-dot rule, and for all others we use
* the rules in the state-man draft instead.
*/
// domain must be either .local or must contain at least
// two dots
if (!value.equals(".local") && value.indexOf('.', 1) == -1)
{
Log.write(Log.COOKI, "Cooki: Bad Set-Cookie header: " + set_cookie +
"\n Domain attribute " + value +
"isn't .local and doesn't have at " +
"least 2 dots");
return false;
}
/** ++GRINDER MODIFICATION **/
// Despite Ronald's comment above, RFC-2965 is not followed in practice.
// Browsers seem to be adopting increasingly complicated heuristics for
// whether a domain matches - see
// http://my.opera.com/yngve/blog/show.dml/26741.
// This is too much for me, I'm removing this check.
if (false) {
/** --GRINDER MODIFICATION **/
// If TLD not special then host minus domain may not
// contain any dots
String top = null;
if (value.length() > 3 )
top = value.substring(value.length()-4);
if (top == null || !(
top.equalsIgnoreCase(".com") ||
top.equalsIgnoreCase(".edu") ||
top.equalsIgnoreCase(".net") ||
top.equalsIgnoreCase(".org") ||
top.equalsIgnoreCase(".gov") ||
top.equalsIgnoreCase(".mil") ||
top.equalsIgnoreCase(".int")))
{
int dl = cookie.domain.length(), vl = value.length();
if (dl > vl &&
cookie.domain.substring(0, dl-vl).indexOf('.') != -1)
{
Log.write(Log.COOKI, "Cooki: Bad Set-Cookie header: " + set_cookie +
"\n Domain attribute " + value +
"is more than one level below " +
"current domain " + cookie.domain);
return false;
}
}
/** ++GRINDER MODIFICATION **/
}
/** --GRINDER MODIFICATION **/
cookie.domain = value;
}
else if (name.equalsIgnoreCase("path"))
cookie.path = value;
else
; // unknown attribute - ignore
return true;
}
/**
* Return the name of this cookie.
*/
public String getName()
{
return name;
}
/**
* Return the value of this cookie.
*/
public String getValue()
{
return value;
}
/**
* @return the expiry date of this cookie, or null if none set.
*/
public Date expires()
{
return expires;
}
/**
* @return true if the cookie should be discarded at the end of the
* session; false otherwise
*/
public boolean discard()
{
return (expires == null);
}
/**
* Return the domain this cookie is valid in.
*/
public String getDomain()
{
return domain;
}
/**
* Return the path this cookie is associated with.
*/
public String getPath()
{
return path;
}
/**
* Return whether this cookie should only be sent over secure connections.
*/
public boolean isSecure()
{
return secure;
}
/**
* @return true if this cookie has expired
*/
public boolean hasExpired()
{
return (expires != null && expires.getTime() <= System.currentTimeMillis());
}
/**
* @param req the request to be sent
* @return true if this cookie should be sent with the request
*/
protected boolean sendWith(RoRequest req)
{
HTTPConnection con = req.getConnection();
String eff_host = con.getHost();
if (eff_host.indexOf('.') == -1) eff_host += ".local";
return ((domain.charAt(0) == '.' && eff_host.endsWith(domain) ||
domain.charAt(0) != '.' && eff_host.equals(domain)) &&
Util.getPath(req.getRequestURI()).startsWith(path) &&
(!secure || con.getProtocol().equals("https") ||
con.getProtocol().equals("shttp")));
}
/**
* Hash up name, path and domain into new hash.
*/
public int hashCode()
{
return (name.hashCode() + path.hashCode() + domain.hashCode());
}
/**
* Two cookies match if the name, path and domain match.
*/
public boolean equals(Object obj)
{
if ((obj != null) && (obj instanceof Cookie))
{
Cookie other = (Cookie) obj;
return (this.name.equals(other.name) &&
this.path.equals(other.path) &&
this.domain.equals(other.domain));
}
return false;
}
/**
* @return a string suitable for sending in a Cookie header.
*/
protected String toExternalForm()
{
return name + "=" + value;
}
/**
* Create a string containing all the cookie fields. The format is that
* used in the Set-Cookie header.
*/
public String toString()
{
StringBuffer res = new StringBuffer(name.length() + value.length() + 30);
res.append(name).append('=').append(value);
if (expires != null) res.append("; expires=").append(expires);
if (path != null) res.append("; path=").append(path);
if (domain != null) res.append("; domain=").append(domain);
if (secure) res.append("; secure");
return res.toString();
}
}