001: /*
002: * $Header: /home/jerenkrantz/tmp/commons/commons-convert/cvs/home/cvs/jakarta-commons//httpclient/src/java/org/apache/commons/httpclient/cookie/CookieSpecBase.java,v 1.28 2004/11/06 19:15:42 mbecke Exp $
003: * $Revision: 480424 $
004: * $Date: 2006-11-29 06:56:49 +0100 (Wed, 29 Nov 2006) $
005: *
006: * ====================================================================
007: *
008: * Licensed to the Apache Software Foundation (ASF) under one or more
009: * contributor license agreements. See the NOTICE file distributed with
010: * this work for additional information regarding copyright ownership.
011: * The ASF licenses this file to You under the Apache License, Version 2.0
012: * (the "License"); you may not use this file except in compliance with
013: * the License. You may obtain a copy of the License at
014: *
015: * http://www.apache.org/licenses/LICENSE-2.0
016: *
017: * Unless required by applicable law or agreed to in writing, software
018: * distributed under the License is distributed on an "AS IS" BASIS,
019: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
020: * See the License for the specific language governing permissions and
021: * limitations under the License.
022: * ====================================================================
023: *
024: * This software consists of voluntary contributions made by many
025: * individuals on behalf of the Apache Software Foundation. For more
026: * information on the Apache Software Foundation, please see
027: * <http://www.apache.org/>.
028: *
029: */
030:
031: package org.apache.commons.httpclient.cookie;
032:
033: import java.util.Collection;
034: import java.util.Date;
035: import java.util.LinkedList;
036: import java.util.List;
037:
038: import org.apache.commons.httpclient.Cookie;
039: import org.apache.commons.httpclient.Header;
040: import org.apache.commons.httpclient.HeaderElement;
041: import org.apache.commons.httpclient.NameValuePair;
042: import org.apache.commons.httpclient.util.DateParseException;
043: import org.apache.commons.httpclient.util.DateUtil;
044: import org.apache.commons.logging.Log;
045: import org.apache.commons.logging.LogFactory;
046:
047: /**
048: *
049: * Cookie management functions shared by all specification.
050: *
051: * @author B.C. Holmes
052: * @author <a href="mailto:jericho@thinkfree.com">Park, Sung-Gu</a>
053: * @author <a href="mailto:dsale@us.britannica.com">Doug Sale</a>
054: * @author Rod Waldhoff
055: * @author dIon Gillard
056: * @author Sean C. Sullivan
057: * @author <a href="mailto:JEvans@Cyveillance.com">John Evans</a>
058: * @author Marc A. Saegesser
059: * @author <a href="mailto:oleg@ural.ru">Oleg Kalnichevski</a>
060: * @author <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a>
061: *
062: * @since 2.0
063: */
064: public class CookieSpecBase implements CookieSpec {
065:
066: /** Log object */
067: protected static final Log LOG = LogFactory
068: .getLog(CookieSpec.class);
069:
070: /** Valid date patterns */
071: private Collection datepatterns = null;
072:
073: /** Default constructor */
074: public CookieSpecBase() {
075: super ();
076: }
077:
078: /**
079: * Parses the Set-Cookie value into an array of <tt>Cookie</tt>s.
080: *
081: * <P>The syntax for the Set-Cookie response header is:
082: *
083: * <PRE>
084: * set-cookie = "Set-Cookie:" cookies
085: * cookies = 1#cookie
086: * cookie = NAME "=" VALUE * (";" cookie-av)
087: * NAME = attr
088: * VALUE = value
089: * cookie-av = "Comment" "=" value
090: * | "Domain" "=" value
091: * | "Max-Age" "=" value
092: * | "Path" "=" value
093: * | "Secure"
094: * | "Version" "=" 1*DIGIT
095: * </PRE>
096: *
097: * @param host the host from which the <tt>Set-Cookie</tt> value was
098: * received
099: * @param port the port from which the <tt>Set-Cookie</tt> value was
100: * received
101: * @param path the path from which the <tt>Set-Cookie</tt> value was
102: * received
103: * @param secure <tt>true</tt> when the <tt>Set-Cookie</tt> value was
104: * received over secure conection
105: * @param header the <tt>Set-Cookie</tt> received from the server
106: * @return an array of <tt>Cookie</tt>s parsed from the Set-Cookie value
107: * @throws MalformedCookieException if an exception occurs during parsing
108: */
109: public Cookie[] parse(String host, int port, String path,
110: boolean secure, final String header)
111: throws MalformedCookieException {
112:
113: LOG.trace("enter CookieSpecBase.parse("
114: + "String, port, path, boolean, Header)");
115:
116: if (host == null) {
117: throw new IllegalArgumentException(
118: "Host of origin may not be null");
119: }
120: if (host.trim().equals("")) {
121: throw new IllegalArgumentException(
122: "Host of origin may not be blank");
123: }
124: if (port < 0) {
125: throw new IllegalArgumentException("Invalid port: " + port);
126: }
127: if (path == null) {
128: throw new IllegalArgumentException(
129: "Path of origin may not be null.");
130: }
131: if (header == null) {
132: throw new IllegalArgumentException(
133: "Header may not be null.");
134: }
135:
136: if (path.trim().equals("")) {
137: path = PATH_DELIM;
138: }
139: host = host.toLowerCase();
140:
141: String defaultPath = path;
142: int lastSlashIndex = defaultPath.lastIndexOf(PATH_DELIM);
143: if (lastSlashIndex >= 0) {
144: if (lastSlashIndex == 0) {
145: //Do not remove the very first slash
146: lastSlashIndex = 1;
147: }
148: defaultPath = defaultPath.substring(0, lastSlashIndex);
149: }
150:
151: HeaderElement[] headerElements = null;
152:
153: boolean isNetscapeCookie = false;
154: int i1 = header.toLowerCase().indexOf("expires=");
155: if (i1 != -1) {
156: i1 += "expires=".length();
157: int i2 = header.indexOf(";", i1);
158: if (i2 == -1) {
159: i2 = header.length();
160: }
161: try {
162: DateUtil.parseDate(header.substring(i1, i2),
163: this .datepatterns);
164: isNetscapeCookie = true;
165: } catch (DateParseException e) {
166: // Does not look like a valid expiry date
167: }
168: }
169: if (isNetscapeCookie) {
170: headerElements = new HeaderElement[] { new HeaderElement(
171: header.toCharArray()) };
172: } else {
173: headerElements = HeaderElement.parseElements(header
174: .toCharArray());
175: }
176:
177: Cookie[] cookies = new Cookie[headerElements.length];
178:
179: for (int i = 0; i < headerElements.length; i++) {
180:
181: HeaderElement headerelement = headerElements[i];
182: Cookie cookie = null;
183: try {
184: cookie = new Cookie(host, headerelement.getName(),
185: headerelement.getValue(), defaultPath, null,
186: false);
187: } catch (IllegalArgumentException e) {
188: throw new MalformedCookieException(e.getMessage());
189: }
190: // cycle through the parameters
191: NameValuePair[] parameters = headerelement.getParameters();
192: // could be null. In case only a header element and no parameters.
193: if (parameters != null) {
194:
195: for (int j = 0; j < parameters.length; j++) {
196: parseAttribute(parameters[j], cookie);
197: }
198: }
199: cookies[i] = cookie;
200: }
201: return cookies;
202: }
203:
204: /**
205: * Parse the <tt>"Set-Cookie"</tt> {@link Header} into an array of {@link
206: * Cookie}s.
207: *
208: * <P>The syntax for the Set-Cookie response header is:
209: *
210: * <PRE>
211: * set-cookie = "Set-Cookie:" cookies
212: * cookies = 1#cookie
213: * cookie = NAME "=" VALUE * (";" cookie-av)
214: * NAME = attr
215: * VALUE = value
216: * cookie-av = "Comment" "=" value
217: * | "Domain" "=" value
218: * | "Max-Age" "=" value
219: * | "Path" "=" value
220: * | "Secure"
221: * | "Version" "=" 1*DIGIT
222: * </PRE>
223: *
224: * @param host the host from which the <tt>Set-Cookie</tt> header was
225: * received
226: * @param port the port from which the <tt>Set-Cookie</tt> header was
227: * received
228: * @param path the path from which the <tt>Set-Cookie</tt> header was
229: * received
230: * @param secure <tt>true</tt> when the <tt>Set-Cookie</tt> header was
231: * received over secure conection
232: * @param header the <tt>Set-Cookie</tt> received from the server
233: * @return an array of <tt>Cookie</tt>s parsed from the <tt>"Set-Cookie"
234: * </tt> header
235: * @throws MalformedCookieException if an exception occurs during parsing
236: */
237: public Cookie[] parse(String host, int port, String path,
238: boolean secure, final Header header)
239: throws MalformedCookieException {
240:
241: LOG.trace("enter CookieSpecBase.parse("
242: + "String, port, path, boolean, String)");
243: if (header == null) {
244: throw new IllegalArgumentException(
245: "Header may not be null.");
246: }
247: return parse(host, port, path, secure, header.getValue());
248: }
249:
250: /**
251: * Parse the cookie attribute and update the corresponsing {@link Cookie}
252: * properties.
253: *
254: * @param attribute {@link HeaderElement} cookie attribute from the
255: * <tt>Set- Cookie</tt>
256: * @param cookie {@link Cookie} to be updated
257: * @throws MalformedCookieException if an exception occurs during parsing
258: */
259:
260: public void parseAttribute(final NameValuePair attribute,
261: final Cookie cookie) throws MalformedCookieException {
262:
263: if (attribute == null) {
264: throw new IllegalArgumentException(
265: "Attribute may not be null.");
266: }
267: if (cookie == null) {
268: throw new IllegalArgumentException(
269: "Cookie may not be null.");
270: }
271: final String paramName = attribute.getName().toLowerCase();
272: String paramValue = attribute.getValue();
273:
274: if (paramName.equals("path")) {
275:
276: if ((paramValue == null) || (paramValue.trim().equals(""))) {
277: paramValue = "/";
278: }
279: cookie.setPath(paramValue);
280: cookie.setPathAttributeSpecified(true);
281:
282: } else if (paramName.equals("domain")) {
283:
284: if (paramValue == null) {
285: throw new MalformedCookieException(
286: "Missing value for domain attribute");
287: }
288: if (paramValue.trim().equals("")) {
289: throw new MalformedCookieException(
290: "Blank value for domain attribute");
291: }
292: cookie.setDomain(paramValue);
293: cookie.setDomainAttributeSpecified(true);
294:
295: } else if (paramName.equals("max-age")) {
296:
297: if (paramValue == null) {
298: throw new MalformedCookieException(
299: "Missing value for max-age attribute");
300: }
301: int age;
302: try {
303: age = Integer.parseInt(paramValue);
304: } catch (NumberFormatException e) {
305: throw new MalformedCookieException("Invalid max-age "
306: + "attribute: " + e.getMessage());
307: }
308: cookie.setExpiryDate(new Date(System.currentTimeMillis()
309: + age * 1000L));
310:
311: } else if (paramName.equals("secure")) {
312:
313: cookie.setSecure(true);
314:
315: } else if (paramName.equals("comment")) {
316:
317: cookie.setComment(paramValue);
318:
319: } else if (paramName.equals("expires")) {
320:
321: if (paramValue == null) {
322: throw new MalformedCookieException(
323: "Missing value for expires attribute");
324: }
325:
326: try {
327: cookie.setExpiryDate(DateUtil.parseDate(paramValue,
328: this .datepatterns));
329: } catch (DateParseException dpe) {
330: LOG.debug("Error parsing cookie date", dpe);
331: throw new MalformedCookieException(
332: "Unable to parse expiration date parameter: "
333: + paramValue);
334: }
335: } else {
336: if (LOG.isDebugEnabled()) {
337: LOG.debug("Unrecognized cookie attribute: "
338: + attribute.toString());
339: }
340: }
341: }
342:
343: public Collection getValidDateFormats() {
344: return this .datepatterns;
345: }
346:
347: public void setValidDateFormats(final Collection datepatterns) {
348: this .datepatterns = datepatterns;
349: }
350:
351: /**
352: * Performs most common {@link Cookie} validation
353: *
354: * @param host the host from which the {@link Cookie} was received
355: * @param port the port from which the {@link Cookie} was received
356: * @param path the path from which the {@link Cookie} was received
357: * @param secure <tt>true</tt> when the {@link Cookie} was received using a
358: * secure connection
359: * @param cookie The cookie to validate.
360: * @throws MalformedCookieException if an exception occurs during
361: * validation
362: */
363:
364: public void validate(String host, int port, String path,
365: boolean secure, final Cookie cookie)
366: throws MalformedCookieException {
367:
368: LOG.trace("enter CookieSpecBase.validate("
369: + "String, port, path, boolean, Cookie)");
370: if (host == null) {
371: throw new IllegalArgumentException(
372: "Host of origin may not be null");
373: }
374: if (host.trim().equals("")) {
375: throw new IllegalArgumentException(
376: "Host of origin may not be blank");
377: }
378: if (port < 0) {
379: throw new IllegalArgumentException("Invalid port: " + port);
380: }
381: if (path == null) {
382: throw new IllegalArgumentException(
383: "Path of origin may not be null.");
384: }
385: if (path.trim().equals("")) {
386: path = PATH_DELIM;
387: }
388: host = host.toLowerCase();
389: // check version
390: if (cookie.getVersion() < 0) {
391: throw new MalformedCookieException(
392: "Illegal version number " + cookie.getValue());
393: }
394:
395: // security check... we musn't allow the server to give us an
396: // invalid domain scope
397:
398: // Validate the cookies domain attribute. NOTE: Domains without
399: // any dots are allowed to support hosts on private LANs that don't
400: // have DNS names. Since they have no dots, to domain-match the
401: // request-host and domain must be identical for the cookie to sent
402: // back to the origin-server.
403: if (host.indexOf(".") >= 0) {
404: // Not required to have at least two dots. RFC 2965.
405: // A Set-Cookie2 with Domain=ajax.com will be accepted.
406:
407: // domain must match host
408: if (!host.endsWith(cookie.getDomain())) {
409: String s = cookie.getDomain();
410: if (s.startsWith(".")) {
411: s = s.substring(1, s.length());
412: }
413: if (!host.equals(s)) {
414: throw new MalformedCookieException(
415: "Illegal domain attribute \""
416: + cookie.getDomain()
417: + "\". Domain of origin: \"" + host
418: + "\"");
419: }
420: }
421: } else {
422: if (!host.equals(cookie.getDomain())) {
423: throw new MalformedCookieException(
424: "Illegal domain attribute \""
425: + cookie.getDomain()
426: + "\". Domain of origin: \"" + host
427: + "\"");
428: }
429: }
430:
431: // another security check... we musn't allow the server to give us a
432: // cookie that doesn't match this path
433:
434: if (!path.startsWith(cookie.getPath())) {
435: throw new MalformedCookieException(
436: "Illegal path attribute \"" + cookie.getPath()
437: + "\". Path of origin: \"" + path + "\"");
438: }
439: }
440:
441: /**
442: * Return <tt>true</tt> if the cookie should be submitted with a request
443: * with given attributes, <tt>false</tt> otherwise.
444: * @param host the host to which the request is being submitted
445: * @param port the port to which the request is being submitted (ignored)
446: * @param path the path to which the request is being submitted
447: * @param secure <tt>true</tt> if the request is using a secure connection
448: * @param cookie {@link Cookie} to be matched
449: * @return true if the cookie matches the criterium
450: */
451:
452: public boolean match(String host, int port, String path,
453: boolean secure, final Cookie cookie) {
454:
455: LOG.trace("enter CookieSpecBase.match("
456: + "String, int, String, boolean, Cookie");
457:
458: if (host == null) {
459: throw new IllegalArgumentException(
460: "Host of origin may not be null");
461: }
462: if (host.trim().equals("")) {
463: throw new IllegalArgumentException(
464: "Host of origin may not be blank");
465: }
466: if (port < 0) {
467: throw new IllegalArgumentException("Invalid port: " + port);
468: }
469: if (path == null) {
470: throw new IllegalArgumentException(
471: "Path of origin may not be null.");
472: }
473: if (cookie == null) {
474: throw new IllegalArgumentException("Cookie may not be null");
475: }
476: if (path.trim().equals("")) {
477: path = PATH_DELIM;
478: }
479: host = host.toLowerCase();
480: if (cookie.getDomain() == null) {
481: LOG.warn("Invalid cookie state: domain not specified");
482: return false;
483: }
484: if (cookie.getPath() == null) {
485: LOG.warn("Invalid cookie state: path not specified");
486: return false;
487: }
488:
489: return
490: // only add the cookie if it hasn't yet expired
491: (cookie.getExpiryDate() == null || cookie.getExpiryDate()
492: .after(new Date()))
493: // and the domain pattern matches
494: && (domainMatch(host, cookie.getDomain()))
495: // and the path is null or matching
496: && (pathMatch(path, cookie.getPath()))
497: // and if the secure flag is set, only if the request is
498: // actually secure
499: && (cookie.getSecure() ? secure : true);
500: }
501:
502: /**
503: * Performs domain-match as implemented in common browsers.
504: * @param host The target host.
505: * @param domain The cookie domain attribute.
506: * @return true if the specified host matches the given domain.
507: */
508: public boolean domainMatch(final String host, String domain) {
509: if (host.equals(domain)) {
510: return true;
511: }
512: if (!domain.startsWith(".")) {
513: domain = "." + domain;
514: }
515: return host.endsWith(domain)
516: || host.equals(domain.substring(1));
517: }
518:
519: /**
520: * Performs path-match as implemented in common browsers.
521: * @param path The target path.
522: * @param topmostPath The cookie path attribute.
523: * @return true if the paths match
524: */
525: public boolean pathMatch(final String path, final String topmostPath) {
526: boolean match = path.startsWith(topmostPath);
527: // if there is a match and these values are not exactly the same we have
528: // to make sure we're not matcing "/foobar" and "/foo"
529: if (match && path.length() != topmostPath.length()) {
530: if (!topmostPath.endsWith(PATH_DELIM)) {
531: match = (path.charAt(topmostPath.length()) == PATH_DELIM_CHAR);
532: }
533: }
534: return match;
535: }
536:
537: /**
538: * Return an array of {@link Cookie}s that should be submitted with a
539: * request with given attributes, <tt>false</tt> otherwise.
540: * @param host the host to which the request is being submitted
541: * @param port the port to which the request is being submitted (currently
542: * ignored)
543: * @param path the path to which the request is being submitted
544: * @param secure <tt>true</tt> if the request is using a secure protocol
545: * @param cookies an array of <tt>Cookie</tt>s to be matched
546: * @return an array of <tt>Cookie</tt>s matching the criterium
547: */
548:
549: public Cookie[] match(String host, int port, String path,
550: boolean secure, final Cookie cookies[]) {
551:
552: LOG.trace("enter CookieSpecBase.match("
553: + "String, int, String, boolean, Cookie[])");
554:
555: if (cookies == null) {
556: return null;
557: }
558: List matching = new LinkedList();
559: for (int i = 0; i < cookies.length; i++) {
560: if (match(host, port, path, secure, cookies[i])) {
561: addInPathOrder(matching, cookies[i]);
562: }
563: }
564: return (Cookie[]) matching.toArray(new Cookie[matching.size()]);
565: }
566:
567: /**
568: * Adds the given cookie into the given list in descending path order. That
569: * is, more specific path to least specific paths. This may not be the
570: * fastest algorythm, but it'll work OK for the small number of cookies
571: * we're generally dealing with.
572: *
573: * @param list - the list to add the cookie to
574: * @param addCookie - the Cookie to add to list
575: */
576: private static void addInPathOrder(List list, Cookie addCookie) {
577: int i = 0;
578:
579: for (i = 0; i < list.size(); i++) {
580: Cookie c = (Cookie) list.get(i);
581: if (addCookie.compare(addCookie, c) > 0) {
582: break;
583: }
584: }
585: list.add(i, addCookie);
586: }
587:
588: /**
589: * Return a string suitable for sending in a <tt>"Cookie"</tt> header
590: * @param cookie a {@link Cookie} to be formatted as string
591: * @return a string suitable for sending in a <tt>"Cookie"</tt> header.
592: */
593: public String formatCookie(Cookie cookie) {
594: LOG.trace("enter CookieSpecBase.formatCookie(Cookie)");
595: if (cookie == null) {
596: throw new IllegalArgumentException("Cookie may not be null");
597: }
598: StringBuffer buf = new StringBuffer();
599: buf.append(cookie.getName());
600: buf.append("=");
601: String s = cookie.getValue();
602: if (s != null) {
603: buf.append(s);
604: }
605: return buf.toString();
606: }
607:
608: /**
609: * Create a <tt>"Cookie"</tt> header value containing all {@link Cookie}s in
610: * <i>cookies</i> suitable for sending in a <tt>"Cookie"</tt> header
611: * @param cookies an array of {@link Cookie}s to be formatted
612: * @return a string suitable for sending in a Cookie header.
613: * @throws IllegalArgumentException if an input parameter is illegal
614: */
615:
616: public String formatCookies(Cookie[] cookies)
617: throws IllegalArgumentException {
618: LOG.trace("enter CookieSpecBase.formatCookies(Cookie[])");
619: if (cookies == null) {
620: throw new IllegalArgumentException(
621: "Cookie array may not be null");
622: }
623: if (cookies.length == 0) {
624: throw new IllegalArgumentException(
625: "Cookie array may not be empty");
626: }
627:
628: StringBuffer buffer = new StringBuffer();
629: for (int i = 0; i < cookies.length; i++) {
630: if (i > 0) {
631: buffer.append("; ");
632: }
633: buffer.append(formatCookie(cookies[i]));
634: }
635: return buffer.toString();
636: }
637:
638: /**
639: * Create a <tt>"Cookie"</tt> {@link Header} containing all {@link Cookie}s
640: * in <i>cookies</i>.
641: * @param cookies an array of {@link Cookie}s to be formatted as a <tt>"
642: * Cookie"</tt> header
643: * @return a <tt>"Cookie"</tt> {@link Header}.
644: */
645: public Header formatCookieHeader(Cookie[] cookies) {
646: LOG.trace("enter CookieSpecBase.formatCookieHeader(Cookie[])");
647: return new Header("Cookie", formatCookies(cookies));
648: }
649:
650: /**
651: * Create a <tt>"Cookie"</tt> {@link Header} containing the {@link Cookie}.
652: * @param cookie <tt>Cookie</tt>s to be formatted as a <tt>Cookie</tt>
653: * header
654: * @return a Cookie header.
655: */
656: public Header formatCookieHeader(Cookie cookie) {
657: LOG.trace("enter CookieSpecBase.formatCookieHeader(Cookie)");
658: return new Header("Cookie", formatCookie(cookie));
659: }
660:
661: }
|