/* This code is part of Freenet. It is distributed under the GNU General * Public License, version 2 (or at your option any later version). See * http://www.gnu.org/ for further details of the GPL. */ package freenet.clients.http; import java.net.URI; import java.net.URISyntaxException; import java.text.ParseException; import java.util.ArrayList; import java.util.Hashtable; import freenet.support.Logger; /** * A cookie which the server has received from the client. * * This class is not thread-safe! * * @author xor (xor@freenetproject.org) */ public final class ReceivedCookie extends Cookie { private static volatile boolean logMINOR; private static volatile boolean logDEBUG; static { Logger.registerClass(ReceivedCookie.class); } private String notValidatedName; private Hashtable<String, String> content; /** * Constructor for creating cookies from parsed key-value pairs. * * Does not validate the names or values of the keys, each attribute is validated at the first call to it's getter method. * Therefore, no CPU time is wasted if the client sends cookies which we do not use. */ private ReceivedCookie(String myName, Hashtable<String, String> myContent) { // We do not validate the input here, we only parse it if someone actually tries to access this cookie. notValidatedName = myName; content = myContent; // We do NOT parse the version even though RFC2965 requires it because Firefox (3.0.14) does not give us a version. version = 1; //version = Integer.parseInt(content.get("$version")); //if(version != 1) // throw new IllegalArgumentException("Invalid version: " + version); } /** * Parses the value of a "Cookie:" HTTP header and returns a list of received cookies which it contained. * - A single "Cookie:" header is allowed to contain multiple cookies. Further, a HTTP request can contain multiple "Cookie" keys!. * * @param httpHeader The value of a "Cookie:" header (i.e. the prefix "Cookie:" must not be contained in this parameter!) * @return A list of {@link ReceivedCookie} objects. The validity of their name/value pairs is not deeply checked, their getName() / getValue() might throw! * @throws ParseException If the general formatting of the cookie is wrong. */ protected static ArrayList<ReceivedCookie> parseHeader(String httpHeader) throws ParseException { if(logMINOR) Logger.minor(ReceivedCookie.class, "Received HTTP cookie header:" + httpHeader); char[] header = httpHeader.toCharArray(); String currentCookieName = null; Hashtable<String,String> currentCookieContent = new Hashtable<String, String>(16); ArrayList<ReceivedCookie> cookies = new ArrayList<ReceivedCookie>(4); // TODO: Adjust to the usual amount of cookies which fred uses + 1 // We do manual parsing instead of using regular expressions for two reasons: // 1. The value of cookies can be quoted, therefore it is a context-free language and not a regular language - we cannot express it with a regexp! // 2. Its very fast :) // Set to true if a broken browser (Konqueror) specifies a cookie where the name is NOT the first attribute. try { for(int i = 0; i < header.length;) { // Skip leading whitespace of key, we must do a header.length check because there might be no more key, so we continue; if(Character.isWhitespace(header[i])) { ++i; continue; } String key; String value = null; // Parse key { int keyBeginIndex = i; while(i < header.length && header[i] != '=' && header[i] != ';') ++i; int keyEndIndex = i; if(keyEndIndex >= header.length || header[keyEndIndex] == ';') value = ""; while(Character.isWhitespace(header[keyEndIndex-1])) // Remove trailing whitespace --keyEndIndex; key = new String(header, keyBeginIndex, keyEndIndex - keyBeginIndex).toLowerCase(); if(key.length() == 0) throw new ParseException("Invalid cookie: Contains an empty key: " + httpHeader, i); // We're done parsing the key, continue to the next character. ++i; } // Parse value (empty values are allowed). if(value == null && i < header.length) { while(Character.isWhitespace(header[i])) // Skip leading whitespace ++i; int valueBeginIndex; char valueEndChar; if(header[i] == '\"') { // Value is quoted valueEndChar = '\"'; valueBeginIndex = ++i; while(header[i] != valueEndChar) ++i; } else { valueEndChar = ';'; valueBeginIndex = i; while(i < header.length && header[i] != valueEndChar) ++i; } int valueEndIndex = i; while(valueEndIndex > valueBeginIndex && Character.isWhitespace(header[valueEndIndex-1])) // Remove trailing whitespace --valueEndIndex; value = new String(header, valueBeginIndex, valueEndIndex - valueBeginIndex); // We're done parsing the value, continue to the next character ++i; // Skip whitespace between end of quotation and the semicolon following the quotation. if(valueEndChar == '\"') { while(i < header.length && header[i] != ';') { if(!Character.isWhitespace(header[i])) throw new ParseException("Invalid cookie: Missing terminating semicolon after value quotation: " + httpHeader, i); ++i; } // We found the semicolon, skip it ++i; } } else value = ""; // RFC2965: Name MUST be first. Anything key besides the name of the cookie begins with $. The next cookie begins if a key occurs which is not // prefixed with $. if(currentCookieName == null) { // We have not found the name yet, the first key/value pair must be the name and the value of the cookie. if(key.charAt(0) == '$') { // We cannot throw because Konqueror (4.2.2) is broken and specifies $version as the first attribute. //throw new IllegalArgumentException("Invalid cookie: Name is not the first attribute: " + httpHeader); currentCookieContent.put(key, value); } else { currentCookieName = key; currentCookieContent.put(currentCookieName, value); } } else { if(key.charAt(0) == '$') currentCookieContent.put(key, value); else {// We finished parsing of the current cookie, a new one starts here. //if(singleCookie) // throw new ParseException("Invalid cookie header: Multiple cookies specified but " // + " the name of the first cookie was not the first attribute: " + httpHeader, i); cookies.add(new ReceivedCookie(currentCookieName, currentCookieContent)); // Store the previous cookie. currentCookieName = key; currentCookieContent = new Hashtable<String, String>(16); currentCookieContent.put(currentCookieName, value); } } } } catch(ArrayIndexOutOfBoundsException e) { ParseException p = new ParseException("Index out of bounds (" + e.getMessage() + ") for cookie " + httpHeader, 0); p.setStackTrace(e.getStackTrace()); throw p; } // Store the last cookie (the loop only stores the current cookie when a new one starts). if(currentCookieName != null) cookies.add(new ReceivedCookie(currentCookieName, currentCookieContent)); return cookies; } /** * @throws IllegalArgumentException If the validation of the name fails. */ @Override public String getName() { if(name == null) { name = validateName(notValidatedName); notValidatedName = null; } return name; } /** * @throws IllegalArgumentException If the validation of the domain fails. */ @Override public URI getDomain() { if(domain == null) { try { String domainString = content.get("$domain"); if(domainString == null) return null; domain = validateDomain(domainString); } catch (URISyntaxException e) { throw new IllegalArgumentException(e); } } return domain; } /** * @throws IllegalArgumentException If the validation of the path fails. */ @Override public URI getPath() { if(path == null) { try { path = validatePath(content.get("$path")); } catch (URISyntaxException e) { throw new IllegalArgumentException(e); } } return path; } /** * @throws IllegalArgumentException If the validation of the name fails. */ @Override public String getValue() { if(value == null) value = validateValue(content.get(getName())); return value; } // TODO: This is broken because TimeUtil.parseHTTPDate() does not work. // public Date getExpirationDate() { // if(expirationDate == null) { // try { // expirationDate = validateExpirationDate(TimeUtil.parseHTTPDate(content.get("$expires"))); // } catch (ParseException e) { // throw new IllegalArgumentException(e); // } // } // // return expirationDate; // } @Override protected String encodeToHeaderValue() { throw new UnsupportedOperationException("ReceivedCookie objects cannot be encoded to a HTTP header value, use Cookie objects!"); } }