001: package com.meterware.httpunit.cookies;
002:
003: /********************************************************************************************************************
004: * $Id: CookieJar.java,v 1.11 2004/09/29 17:15:26 russgold Exp $
005: *
006: * Copyright (c) 2002-2004, Russell Gold
007: *
008: * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
009: * documentation files (the "Software"), to deal in the Software without restriction, including without limitation
010: * the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and
011: * to permit persons to whom the Software is furnished to do so, subject to the following conditions:
012: *
013: * The above copyright notice and this permission notice shall be included in all copies or substantial portions
014: * of the Software.
015: *
016: * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
017: * THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
018: * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
019: * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
020: * DEALINGS IN THE SOFTWARE.
021: *
022: *******************************************************************************************************************/
023: import java.io.IOException;
024: import java.io.StreamTokenizer;
025: import java.io.StringReader;
026: import java.net.URL;
027: import java.util.*;
028:
029: /**
030: * A collection of HTTP cookies, which can interact with cookie and set-cookie header values.
031: *
032: * @author <a href="mailto:russgold@httpunit.org">Russell Gold</a>
033: * @author <a href="mailto:drew.varner@oracle.com">Drew Varner</a>
034: **/
035: public class CookieJar {
036:
037: private static final int DEFAULT_HEADER_SIZE = 80;
038:
039: private ArrayList _cookies = new ArrayList();
040: private ArrayList _globalCookies = new ArrayList();
041: private CookiePress _press;
042:
043: /**
044: * Creates an empty cookie jar.
045: */
046: public CookieJar() {
047: _press = new CookiePress(null);
048: }
049:
050: /**
051: * Creates a cookie jar which is initially populated with cookies parsed from the <code>Set-Cookie</code> and
052: * <code>Set-Cookie2</code> header fields.
053: * <p>
054: * Note that the parsing does not strictly follow the specifications, but
055: * attempts to imitate the behavior of popular browsers. Specifically,
056: * it allows cookie values to contain commas, which the
057: * Netscape standard does not allow for, but which is required by some servers.
058: * </p>
059: */
060: public CookieJar(CookieSource source) {
061: _press = new CookiePress(source.getURL());
062: findCookies(source.getHeaderFields("Set-Cookie"),
063: new RFC2109CookieRecipe());
064: findCookies(source.getHeaderFields("Set-Cookie2"),
065: new RFC2965CookieRecipe());
066: }
067:
068: private void findCookies(String cookieHeader[], CookieRecipe recipe) {
069: for (int i = 0; i < cookieHeader.length; i++) {
070: recipe.findCookies(cookieHeader[i]);
071: }
072: }
073:
074: /**
075: * Empties this cookie jar of all contents.
076: */
077: public void clear() {
078: _cookies.clear();
079: _globalCookies.clear();
080: }
081:
082: /**
083: * Defines a cookie to be sent to the server on every request. This bypasses the normal mechanism by which only
084: * certain cookies are sent based on their host and path.
085: * @deprecated as of 1.6, use #putCookie
086: **/
087: public void addCookie(String name, String value) {
088: _globalCookies.add(new Cookie(name, value));
089: }
090:
091: /**
092: * Defines a cookie to be sent to the server on every request. This bypasses the normal mechanism by which only
093: * certain cookies are sent based on their host and path.
094: * @since 1.6
095: **/
096: public void putCookie(String name, String value) {
097: for (Iterator iterator = _globalCookies.iterator(); iterator
098: .hasNext();) {
099: Cookie cookie = (Cookie) iterator.next();
100: if (name.equals(cookie.getName()))
101: iterator.remove();
102: }
103: _globalCookies.add(new Cookie(name, value));
104: }
105:
106: /**
107: * Returns the name of all the active cookies in this cookie jar.
108: **/
109: public String[] getCookieNames() {
110: final int numGlobalCookies = _globalCookies.size();
111: String[] names = new String[_cookies.size() + numGlobalCookies];
112: for (int i = 0; i < numGlobalCookies; i++) {
113: names[i] = ((Cookie) _globalCookies.get(i)).getName();
114: }
115: for (int i = numGlobalCookies; i < names.length; i++) {
116: names[i] = ((Cookie) _cookies.get(i - numGlobalCookies))
117: .getName();
118: }
119: return names;
120: }
121:
122: /**
123: * Returns a collection containing all of the cookies in this jar.
124: */
125: public Collection getCookies() {
126: final Collection collection = (Collection) _cookies.clone();
127: collection.addAll(_globalCookies);
128: return collection;
129: }
130:
131: /**
132: * Returns the value of the specified cookie.
133: **/
134: public String getCookieValue(String name) {
135: Cookie cookie = getCookie(name);
136: return cookie == null ? null : cookie.getValue();
137: }
138:
139: /**
140: * Returns the value of the specified cookie.
141: **/
142: public Cookie getCookie(String name) {
143: if (name == null)
144: throw new IllegalArgumentException(
145: "getCookieValue: no name specified");
146: for (Iterator iterator = _cookies.iterator(); iterator
147: .hasNext();) {
148: Cookie cookie = (Cookie) iterator.next();
149: if (name.equals(cookie.getName()))
150: return cookie;
151: }
152: for (Iterator iterator = _globalCookies.iterator(); iterator
153: .hasNext();) {
154: Cookie cookie = (Cookie) iterator.next();
155: if (name.equals(cookie.getName()))
156: return cookie;
157: }
158: return null;
159: }
160:
161: /**
162: * Returns the value of the cookie header to be sent to the specified URL.
163: * Will return null if no compatible cookie is defined.
164: **/
165: public String getCookieHeaderField(URL targetURL) {
166: if (_cookies.isEmpty() && _globalCookies.isEmpty())
167: return null;
168: StringBuffer sb = new StringBuffer(DEFAULT_HEADER_SIZE);
169: HashSet restrictedCookies = new HashSet();
170: for (Iterator i = _cookies.iterator(); i.hasNext();) {
171: Cookie cookie = (Cookie) i.next();
172: if (!cookie.mayBeSentTo(targetURL))
173: continue;
174: restrictedCookies.add(cookie.getName());
175: if (sb.length() != 0)
176: sb.append("; ");
177: sb.append(cookie.getName()).append('=').append(
178: cookie.getValue());
179: }
180: for (Iterator i = _globalCookies.iterator(); i.hasNext();) {
181: Cookie cookie = (Cookie) i.next();
182: if (restrictedCookies.contains(cookie.getName()))
183: continue;
184: if (sb.length() != 0)
185: sb.append("; ");
186: sb.append(cookie.getName()).append('=').append(
187: cookie.getValue());
188: }
189: return sb.length() == 0 ? null : sb.toString();
190: }
191:
192: /**
193: * Updates the cookies maintained in this cookie jar with those in another cookie jar. Any duplicate cookies in
194: * the new jar will replace those in this jar.
195: **/
196: public void updateCookies(CookieJar newJar) {
197: for (Iterator i = newJar._cookies.iterator(); i.hasNext();) {
198: addUniqueCookie((Cookie) i.next());
199: }
200: }
201:
202: /**
203: * Add the cookie to this jar, replacing any previous matching cookie.
204: */
205: void addUniqueCookie(Cookie cookie) {
206: _cookies.remove(cookie);
207: _cookies.add(cookie);
208: }
209:
210: abstract class CookieRecipe {
211:
212: /**
213: * Extracts cookies from a cookie header. Works in conjunction with a cookie press class, which actually creates
214: * the cookies and adds them to the jar as appropriate.
215: *
216: * 1. Parse the header into tokens, separated by ',' and ';' (respecting single and double quotes)
217: * 2. Process tokens from the end:
218: * a. if the token contains an '=' we have a name/value pair. Add them to the cookie press, which
219: * will decide if it is a cookie name or an attribute name.
220: * b. if the token is a reserved word, flush the cookie press and continue.
221: * c. otherwise, add the token to the cookie press, passing along the last character of the previous token.
222: */
223: void findCookies(String cookieHeader) {
224: Vector tokens = getCookieTokens(cookieHeader);
225:
226: for (int i = tokens.size() - 1; i >= 0; i--) {
227: String token = (String) tokens.elementAt(i);
228:
229: int equalsIndex = getEqualsIndex(token);
230: if (equalsIndex != -1) {
231: _press.addTokenWithEqualsSign(this , token,
232: equalsIndex);
233: } else if (isCookieReservedWord(token)) {
234: _press.clear();
235: } else {
236: _press.addToken(token, lastCharOf((i == 0) ? ""
237: : (String) tokens.elementAt(i - 1)));
238: }
239: }
240: }
241:
242: private char lastCharOf(String string) {
243: return (string.length() == 0) ? ' ' : string.charAt(string
244: .length() - 1);
245: }
246:
247: /**
248: * Returns the index (if any) of the equals sign separating a cookie name from the its value.
249: * Equals signs at the end of the token are ignored in this calculation, since they may be
250: * part of a Base64-encoded value.
251: */
252: private int getEqualsIndex(String token) {
253: if (!token.endsWith("==")) {
254: return token.indexOf('=');
255: } else {
256: return getEqualsIndex(token.substring(0,
257: token.length() - 2));
258: }
259: }
260:
261: /**
262: * Tokenizes a cookie header and returns the tokens in a
263: * <code>Vector</code>.
264: **/
265: private Vector getCookieTokens(String cookieHeader) {
266: StringReader sr = new StringReader(cookieHeader);
267: StreamTokenizer st = new StreamTokenizer(sr);
268: Vector tokens = new Vector();
269:
270: // clear syntax tables of the StreamTokenizer
271: st.resetSyntax();
272:
273: // set all characters as word characters
274: st.wordChars(0, Character.MAX_VALUE);
275:
276: // set up characters for quoting
277: st.quoteChar('"'); //double quotes
278: st.quoteChar('\''); //single quotes
279:
280: // set up characters to separate tokens
281: st.whitespaceChars(59, 59); //semicolon
282: st.whitespaceChars(44, 44); //comma
283:
284: try {
285: while (st.nextToken() != StreamTokenizer.TT_EOF) {
286: tokens.addElement(st.sval.trim());
287: }
288: } catch (IOException ioe) {
289: // this will never happen with a StringReader
290: }
291: sr.close();
292: return tokens;
293: }
294:
295: abstract protected boolean isCookieAttribute(
296: String stringLowercase);
297:
298: abstract protected boolean isCookieReservedWord(String token);
299:
300: }
301:
302: class CookiePress {
303:
304: private StringBuffer _value = new StringBuffer();
305: private HashMap _attributes = new HashMap();
306: private URL _sourceURL;
307:
308: public CookiePress(URL sourceURL) {
309: _sourceURL = sourceURL;
310: }
311:
312: void clear() {
313: _value.setLength(0);
314: _attributes.clear();
315: }
316:
317: void addToken(String token, char lastChar) {
318: _value.insert(0, token);
319: if (lastChar != '=')
320: _value.insert(0, ',');
321: }
322:
323: void addTokenWithEqualsSign(CookieRecipe recipe, String token,
324: int equalsIndex) {
325: String name = token.substring(0, equalsIndex).trim();
326: _value.insert(0, token.substring(equalsIndex + 1).trim());
327: if (recipe.isCookieAttribute(name.toLowerCase())) {
328: _attributes.put(name.toLowerCase(), _value.toString());
329: } else {
330: addCookieIfValid(new Cookie(name, _value.toString(),
331: _attributes));
332: _attributes.clear();
333: }
334: _value.setLength(0);
335: }
336:
337: private void addCookieIfValid(Cookie cookie) {
338: if (acceptCookie(cookie))
339: addUniqueCookie(cookie);
340: }
341:
342: private boolean acceptCookie(Cookie cookie) {
343: if (cookie.getPath() == null) {
344: cookie.setPath(getParentPath(_sourceURL.getPath()));
345: } else {
346: int status = getPathAttributeStatus(cookie.getPath(),
347: _sourceURL.getPath());
348: if (status != CookieListener.ACCEPTED) {
349: reportCookieRejected(status, cookie.getPath(),
350: cookie.getName());
351: return false;
352: }
353: }
354:
355: if (cookie.getDomain() == null) {
356: cookie.setDomain(_sourceURL.getHost());
357: } else if (!CookieProperties.isDomainMatchingStrict()
358: && cookie.getDomain().equalsIgnoreCase(
359: _sourceURL.getHost())) {
360: cookie.setDomain(_sourceURL.getHost());
361: } else {
362: int status = getDomainAttributeStatus(cookie
363: .getDomain(), _sourceURL.getHost());
364: if (status != CookieListener.ACCEPTED) {
365: reportCookieRejected(status, cookie.getDomain(),
366: cookie.getName());
367: return false;
368: }
369: }
370:
371: return true;
372: }
373:
374: private String getParentPath(String path) {
375: int rightmostSlashIndex = path.lastIndexOf('/');
376: return rightmostSlashIndex < 0 ? "/" : path.substring(0,
377: rightmostSlashIndex);
378: }
379:
380: private int getPathAttributeStatus(String pathAttribute,
381: String sourcePath) {
382: if (!CookieProperties.isPathMatchingStrict()
383: || sourcePath.length() == 0
384: || sourcePath.startsWith(pathAttribute)) {
385: return CookieListener.ACCEPTED;
386: } else {
387: return CookieListener.PATH_NOT_PREFIX;
388: }
389: }
390:
391: private int getDomainAttributeStatus(String domainAttribute,
392: String sourceHost) {
393: if (!domainAttribute.startsWith("."))
394: domainAttribute = '.' + domainAttribute;
395:
396: if (domainAttribute.lastIndexOf('.') == 0) {
397: return CookieListener.DOMAIN_ONE_DOT;
398: } else if (!sourceHost.endsWith(domainAttribute)) {
399: return CookieListener.DOMAIN_NOT_SOURCE_SUFFIX;
400: } else if (CookieProperties.isDomainMatchingStrict()
401: && sourceHost.lastIndexOf(domainAttribute) > sourceHost
402: .indexOf('.')) {
403: return CookieListener.DOMAIN_TOO_MANY_LEVELS;
404: } else {
405: return CookieListener.ACCEPTED;
406: }
407: }
408:
409: private boolean reportCookieRejected(int reason,
410: String attribute, String source) {
411: CookieProperties.reportCookieRejected(reason, attribute,
412: source);
413: return false;
414: }
415:
416: }
417:
418: /**
419: * Parses cookies according to
420: * <a href="http://www.ietf.org/rfc/rfc2109.txt">RFC 2109</a>
421: *
422: * <br />
423: * These cookies come from the <code>Set-Cookie:</code> header
424: **/
425: class RFC2109CookieRecipe extends CookieRecipe {
426:
427: protected boolean isCookieAttribute(String stringLowercase) {
428: return stringLowercase.equals("path")
429: || stringLowercase.equals("domain")
430: || stringLowercase.equals("expires")
431: || stringLowercase.equals("comment")
432: || stringLowercase.equals("max-age")
433: || stringLowercase.equals("version");
434: }
435:
436: protected boolean isCookieReservedWord(String token) {
437: return token.equalsIgnoreCase("secure");
438: }
439: }
440:
441: /**
442: * Parses cookies according to
443: * <a href="http://www.ietf.org/rfc/rfc2965.txt">RFC 2965</a>
444: *
445: * <br />
446: * These cookies come from the <code>Set-Cookie2:</code> header
447: **/
448: class RFC2965CookieRecipe extends CookieRecipe {
449:
450: protected boolean isCookieAttribute(String stringLowercase) {
451: return stringLowercase.equals("path")
452: || stringLowercase.equals("domain")
453: || stringLowercase.equals("comment")
454: || stringLowercase.equals("commenturl")
455: || stringLowercase.equals("max-age")
456: || stringLowercase.equals("version")
457: || stringLowercase.equals("$version")
458: || stringLowercase.equals("port");
459: }
460:
461: protected boolean isCookieReservedWord(String token) {
462: return token.equalsIgnoreCase("discard")
463: || token.equalsIgnoreCase("secure");
464: }
465: }
466:
467: }
|