View Javadoc

1   /*
2    * $HeadURL: https://svn.apache.org/repos/asf/jakarta/commons/proper/httpclient/trunk/src/java/org/apache/commons/httpclient/cookie/RFC2965Spec.java $
3    * $Revision: 400312 $
4    * $Date: 2006-05-06 14:49:41 +0200 (Sat, 06 May 2006) $
5    * 
6    * ====================================================================
7    *
8    *  Copyright 2002-2004 The Apache Software Foundation
9    *
10   *  Licensed under the Apache License, Version 2.0 (the "License");
11   *  you may not use this file except in compliance with the License.
12   *  You may obtain a copy of the License at
13   *
14   *      http://www.apache.org/licenses/LICENSE-2.0
15   *
16   *  Unless required by applicable law or agreed to in writing, software
17   *  distributed under the License is distributed on an "AS IS" BASIS,
18   *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19   *  See the License for the specific language governing permissions and
20   *  limitations under the License.
21   * ====================================================================
22   *
23   * This software consists of voluntary contributions made by many
24   * individuals on behalf of the Apache Software Foundation.  For more
25   * information on the Apache Software Foundation, please see
26   * <http://www.apache.org/>.
27   *
28   */
29  
30  package org.apache.commons.httpclient.cookie;
31  
32  import java.util.ArrayList;
33  import java.util.Arrays;
34  import java.util.Comparator;
35  import java.util.Date;
36  import java.util.HashMap;
37  import java.util.Iterator;
38  import java.util.LinkedList;
39  import java.util.List;
40  import java.util.Map;
41  import java.util.StringTokenizer;
42  
43  import org.apache.commons.httpclient.Cookie;
44  import org.apache.commons.httpclient.Header;
45  import org.apache.commons.httpclient.HeaderElement;
46  import org.apache.commons.httpclient.NameValuePair;
47  import org.apache.commons.httpclient.util.ParameterFormatter;
48  
49  /***
50   * <p>RFC 2965 specific cookie management functions.</p>
51   * 
52   * @author jain.samit@gmail.com (Samit Jain)
53   *
54   * @since 3.1
55   */
56  public class RFC2965Spec extends CookieSpecBase implements CookieVersionSupport {
57  
58      private static final Comparator PATH_COMPOARATOR = new CookiePathComparator();
59      
60      /***
61      * Cookie Response Header  name for cookies processed
62      * by this spec.
63      */
64      public static String SET_COOKIE2_KEY = "set-cookie2";
65      
66      /***
67      * used for formatting RFC 2956 style cookies
68      */
69      private final ParameterFormatter formatter;
70       
71      /***
72       * Stores the list of attribute handlers
73       */
74      private final List attribHandlerList;
75      
76      /***
77      * Stores attribute name -> attribute handler mappings
78      */
79      private final Map attribHandlerMap;
80  
81      /***
82       * Fallback cookie spec (RFC 2109)
83       */
84      private final CookieSpec rfc2109;
85      
86      /*** 
87       * Default constructor 
88       * */
89      public RFC2965Spec() {
90          super();
91          this.formatter = new ParameterFormatter();
92          this.formatter.setAlwaysUseQuotes(true);
93          this.attribHandlerMap = new HashMap(10);        
94          this.attribHandlerList = new ArrayList(10);
95          this.rfc2109 = new RFC2109Spec();
96          
97          registerAttribHandler(Cookie2.PATH, new Cookie2PathAttributeHandler());
98          registerAttribHandler(Cookie2.DOMAIN, new Cookie2DomainAttributeHandler());
99          registerAttribHandler(Cookie2.PORT, new Cookie2PortAttributeHandler());
100         registerAttribHandler(Cookie2.MAXAGE, new Cookie2MaxageAttributeHandler());
101         registerAttribHandler(Cookie2.SECURE, new CookieSecureAttributeHandler());
102         registerAttribHandler(Cookie2.COMMENT, new CookieCommentAttributeHandler());
103         registerAttribHandler(Cookie2.COMMENTURL, new CookieCommentUrlAttributeHandler());
104         registerAttribHandler(Cookie2.DISCARD, new CookieDiscardAttributeHandler());
105         registerAttribHandler(Cookie2.VERSION, new Cookie2VersionAttributeHandler());
106     }
107 
108     protected void registerAttribHandler(
109             final String name, final CookieAttributeHandler handler) {
110         if (name == null) {
111             throw new IllegalArgumentException("Attribute name may not be null");
112         }
113         if (handler == null) {
114             throw new IllegalArgumentException("Attribute handler may not be null");
115         }
116         if (!this.attribHandlerList.contains(handler)) {
117             this.attribHandlerList.add(handler);
118         }
119         this.attribHandlerMap.put(name, handler);
120     }
121     
122     /***
123      * Finds an attribute handler {@link CookieAttributeHandler} for the
124      * given attribute. Returns <tt>null</tt> if no attribute handler is
125      * found for the specified attribute.
126      *
127      * @param name attribute name. e.g. Domain, Path, etc.
128      * @return an attribute handler or <tt>null</tt>
129      */
130     protected CookieAttributeHandler findAttribHandler(final String name) {
131         return (CookieAttributeHandler) this.attribHandlerMap.get(name);
132     }
133     
134     /***
135      * Gets attribute handler {@link CookieAttributeHandler} for the
136      * given attribute.
137      *
138      * @param name attribute name. e.g. Domain, Path, etc.
139      * @throws IllegalStateException if handler not found for the
140      *          specified attribute.
141      */
142     protected CookieAttributeHandler getAttribHandler(final String name) {
143         CookieAttributeHandler handler = findAttribHandler(name);
144         if (handler == null) {
145             throw new IllegalStateException("Handler not registered for " +
146                                             name + " attribute.");
147         } else {
148             return handler;
149         }
150     }
151 
152     protected Iterator getAttribHandlerIterator() {
153         return this.attribHandlerList.iterator();
154     }
155     
156     /***
157      * Parses the Set-Cookie2 value into an array of <tt>Cookie</tt>s.
158      *
159      * <P>The syntax for the Set-Cookie2 response header is:
160      *
161      * <PRE>
162      * set-cookie      =    "Set-Cookie2:" cookies
163      * cookies         =    1#cookie
164      * cookie          =    NAME "=" VALUE * (";" cookie-av)
165      * NAME            =    attr
166      * VALUE           =    value
167      * cookie-av       =    "Comment" "=" value
168      *                 |    "CommentURL" "=" <"> http_URL <">
169      *                 |    "Discard"
170      *                 |    "Domain" "=" value
171      *                 |    "Max-Age" "=" value
172      *                 |    "Path" "=" value
173      *                 |    "Port" [ "=" <"> portlist <"> ]
174      *                 |    "Secure"
175      *                 |    "Version" "=" 1*DIGIT
176      * portlist        =       1#portnum
177      * portnum         =       1*DIGIT
178      * </PRE>
179      *
180      * @param host the host from which the <tt>Set-Cookie2</tt> value was
181      * received
182      * @param port the port from which the <tt>Set-Cookie2</tt> value was
183      * received
184      * @param path the path from which the <tt>Set-Cookie2</tt> value was
185      * received
186      * @param secure <tt>true</tt> when the <tt>Set-Cookie2</tt> value was
187      * received over secure conection
188      * @param header the <tt>Set-Cookie2</tt> <tt>Header</tt> received from the server
189      * @return an array of <tt>Cookie</tt>s parsed from the Set-Cookie2 value
190      * @throws MalformedCookieException if an exception occurs during parsing
191      */
192     public Cookie[] parse(
193             String host, int port, String path, boolean secure, final Header header)
194             throws MalformedCookieException {
195         LOG.trace("enter RFC2965.parse("
196                   + "String, int, String, boolean, Header)");
197 
198         if (header == null) {
199             throw new IllegalArgumentException("Header may not be null.");
200         }
201         if (header.getName() == null) {
202             throw new IllegalArgumentException("Header name may not be null.");
203         }
204 
205         if (header.getName().equalsIgnoreCase(SET_COOKIE2_KEY)) {
206             // parse cookie2 cookies
207             return parse(host, port, path, secure, header.getValue());
208         } else if (header.getName().equalsIgnoreCase(RFC2109Spec.SET_COOKIE_KEY)) {
209             // delegate parsing of old-style cookies to rfc2109Spec
210             return this.rfc2109.parse(host, port, path, secure, header.getValue());
211         } else {
212             throw new MalformedCookieException("Header name is not valid. " +
213                                                "RFC 2965 supports \"set-cookie\" " +
214                                                "and \"set-cookie2\" headers.");
215         }
216     }
217 
218     /***
219      * @see #parse(String, int, String, boolean, org.apache.commons.httpclient.Header)
220      */
221     public Cookie[] parse(String host, int port, String path,
222                           boolean secure, final String header)
223             throws MalformedCookieException {
224         LOG.trace("enter RFC2965Spec.parse("
225                   + "String, int, String, boolean, String)");
226 
227         // before we do anything, lets check validity of arguments
228         if (host == null) {
229             throw new IllegalArgumentException(
230                     "Host of origin may not be null");
231         }
232         if (host.trim().equals("")) {
233             throw new IllegalArgumentException(
234                     "Host of origin may not be blank");
235         }
236         if (port < 0) {
237             throw new IllegalArgumentException("Invalid port: " + port);
238         }
239         if (path == null) {
240             throw new IllegalArgumentException(
241                     "Path of origin may not be null.");
242         }
243         if (header == null) {
244             throw new IllegalArgumentException("Header may not be null.");
245         }
246 
247         if (path.trim().equals("")) {
248             path = PATH_DELIM;
249         }
250         host = getEffectiveHost(host);
251 
252         HeaderElement[] headerElements =
253                 HeaderElement.parseElements(header.toCharArray());
254 
255         List cookies = new LinkedList();
256         for (int i = 0; i < headerElements.length; i++) {
257             HeaderElement headerelement = headerElements[i];
258             Cookie2 cookie = null;
259             try {
260                 cookie = new Cookie2(host,
261                                     headerelement.getName(),
262                                     headerelement.getValue(),
263                                     path,
264                                     null,
265                                     false,
266                                     new int[] {port});
267             } catch (IllegalArgumentException ex) {
268                 throw new MalformedCookieException(ex.getMessage());
269             }
270             NameValuePair[] parameters = headerelement.getParameters();
271             // could be null. In case only a header element and no parameters.
272             if (parameters != null) {
273                 // Eliminate duplicate attribues. The first occurence takes precedence
274                 Map attribmap = new HashMap(parameters.length); 
275                 for (int j = parameters.length - 1; j >= 0; j--) {
276                     NameValuePair param = parameters[j];
277                     attribmap.put(param.getName().toLowerCase(), param);
278                 }
279                 for (Iterator it = attribmap.entrySet().iterator(); it.hasNext(); ) {
280                     Map.Entry entry = (Map.Entry) it.next();
281                     parseAttribute((NameValuePair) entry.getValue(), cookie);
282                 }
283             }
284             cookies.add(cookie);
285             // cycle through the parameters
286         }
287         return (Cookie[]) cookies.toArray(new Cookie[cookies.size()]);
288     }
289 
290     /***
291      * Parse RFC 2965 specific cookie attribute and update the corresponsing
292      * {@link org.apache.commons.httpclient.Cookie} properties.
293      *
294      * @param attribute {@link org.apache.commons.httpclient.NameValuePair} cookie attribute from the
295      * <tt>Set-Cookie2</tt> header.
296      * @param cookie {@link org.apache.commons.httpclient.Cookie} to be updated
297      * @throws MalformedCookieException if an exception occurs during parsing
298      */
299     public void parseAttribute(
300             final NameValuePair attribute, final Cookie cookie)
301             throws MalformedCookieException {
302         if (attribute == null) {
303             throw new IllegalArgumentException("Attribute may not be null.");
304         }
305         if (attribute.getName() == null) {
306             throw new IllegalArgumentException("Attribute Name may not be null.");
307         }
308         if (cookie == null) {
309             throw new IllegalArgumentException("Cookie may not be null.");
310         }
311         final String paramName = attribute.getName().toLowerCase();
312         final String paramValue = attribute.getValue();
313 
314         CookieAttributeHandler handler = findAttribHandler(paramName);
315         if (handler == null) {
316             // ignore unknown attribute-value pairs
317             if (LOG.isDebugEnabled())
318                 LOG.debug("Unrecognized cookie attribute: " +
319                           attribute.toString());
320         } else {
321             handler.parse(cookie, paramValue);
322         }
323     }
324 
325     /***
326      * Performs RFC 2965 compliant {@link org.apache.commons.httpclient.Cookie} validation
327      *
328      * @param host the host from which the {@link org.apache.commons.httpclient.Cookie} was received
329      * @param port the port from which the {@link org.apache.commons.httpclient.Cookie} was received
330      * @param path the path from which the {@link org.apache.commons.httpclient.Cookie} was received
331      * @param secure <tt>true</tt> when the {@link org.apache.commons.httpclient.Cookie} was received using a
332      * secure connection
333      * @param cookie The cookie to validate
334      * @throws MalformedCookieException if an exception occurs during
335      * validation
336      */
337     public void validate(final String host, int port, final String path,
338                          boolean secure, final Cookie cookie)
339             throws MalformedCookieException {
340 
341         LOG.trace("enter RFC2965Spec.validate(String, int, String, "
342                   + "boolean, Cookie)");
343 
344         if (cookie instanceof Cookie2) {
345             if (cookie.getName().indexOf(' ') != -1) {
346                 throw new MalformedCookieException("Cookie name may not contain blanks");
347             }
348             if (cookie.getName().startsWith("$")) {
349                 throw new MalformedCookieException("Cookie name may not start with $");
350             }
351             CookieOrigin origin = new CookieOrigin(getEffectiveHost(host), port, path, secure); 
352             for (Iterator i = getAttribHandlerIterator(); i.hasNext(); ) {
353               CookieAttributeHandler handler = (CookieAttributeHandler) i.next();
354               handler.validate(cookie, origin);
355             }
356         } else {
357             // old-style cookies are validated according to the old rules
358             this.rfc2109.validate(host, port, path, secure, cookie);
359         }
360     }
361 
362     /***
363      * Return <tt>true</tt> if the cookie should be submitted with a request
364      * with given attributes, <tt>false</tt> otherwise.
365      * @param host the host to which the request is being submitted
366      * @param port the port to which the request is being submitted (ignored)
367      * @param path the path to which the request is being submitted
368      * @param secure <tt>true</tt> if the request is using a secure connection
369      * @return true if the cookie matches the criterium
370      */
371     public boolean match(String host, int port, String path,
372                          boolean secure, final Cookie cookie) {
373 
374         LOG.trace("enter RFC2965.match("
375                   + "String, int, String, boolean, Cookie");
376         if (cookie == null) {
377             throw new IllegalArgumentException("Cookie may not be null");
378         }
379         if (cookie instanceof Cookie2) {
380             // check if cookie has expired
381             if (cookie.isPersistent() && cookie.isExpired()) {
382                 return false;
383             }
384             CookieOrigin origin = new CookieOrigin(getEffectiveHost(host), port, path, secure); 
385             for (Iterator i = getAttribHandlerIterator(); i.hasNext(); ) {
386                 CookieAttributeHandler handler = (CookieAttributeHandler) i.next();
387                 if (!handler.match(cookie, origin)) {
388                     return false;
389                 }
390             }
391             return true;
392         } else {
393             // old-style cookies are matched according to the old rules
394             return this.rfc2109.match(host, port, path, secure, cookie);
395         }
396     }
397 
398     private void doFormatCookie2(final Cookie2 cookie, final StringBuffer buffer) {
399         String name = cookie.getName();
400         String value = cookie.getValue();
401         if (value == null) {
402             value = "";
403         }
404         this.formatter.format(buffer, new NameValuePair(name, value));
405         // format domain attribute
406         if (cookie.getDomain() != null && cookie.isDomainAttributeSpecified()) {
407             buffer.append("; ");
408             this.formatter.format(buffer, new NameValuePair("$Domain", cookie.getDomain()));
409         }
410         // format path attribute
411         if ((cookie.getPath() != null) && (cookie.isPathAttributeSpecified())) {
412             buffer.append("; ");
413             this.formatter.format(buffer, new NameValuePair("$Path", cookie.getPath()));
414         }
415         // format port attribute
416         if (cookie.isPortAttributeSpecified()) {
417             String portValue = "";
418             if (!cookie.isPortAttributeBlank()) {
419                 portValue = createPortAttribute(cookie.getPorts());
420             }
421             buffer.append("; ");
422             this.formatter.format(buffer, new NameValuePair("$Port", portValue));
423         }
424     }
425     
426     /***
427      * Return a string suitable for sending in a <tt>"Cookie"</tt> header as
428      * defined in RFC 2965
429      * @param cookie a {@link org.apache.commons.httpclient.Cookie} to be formatted as string
430      * @return a string suitable for sending in a <tt>"Cookie"</tt> header.
431      */
432     public String formatCookie(final Cookie cookie) {
433         LOG.trace("enter RFC2965Spec.formatCookie(Cookie)");
434 
435         if (cookie == null) {
436             throw new IllegalArgumentException("Cookie may not be null");
437         }
438         if (cookie instanceof Cookie2) {
439             Cookie2 cookie2 = (Cookie2) cookie;
440             int version = cookie2.getVersion();
441             final StringBuffer buffer = new StringBuffer();
442             this.formatter.format(buffer, new NameValuePair("$Version", Integer.toString(version)));
443             buffer.append("; ");
444             doFormatCookie2(cookie2, buffer);
445             return buffer.toString();
446         } else {
447             // old-style cookies are formatted according to the old rules
448             return this.rfc2109.formatCookie(cookie);
449         }
450     }
451 
452     /***
453      * Create a RFC 2965 compliant <tt>"Cookie"</tt> header value containing all
454      * {@link org.apache.commons.httpclient.Cookie}s suitable for
455      * sending in a <tt>"Cookie"</tt> header
456      * @param cookies an array of {@link org.apache.commons.httpclient.Cookie}s to be formatted
457      * @return a string suitable for sending in a Cookie header.
458      */
459     public String formatCookies(final Cookie[] cookies) {
460         LOG.trace("enter RFC2965Spec.formatCookieHeader(Cookie[])");
461 
462         if (cookies == null) {
463             throw new IllegalArgumentException("Cookies may not be null");
464         }
465         // check if cookies array contains a set-cookie (old style) cookie
466         boolean hasOldStyleCookie = false;
467         int version = -1;
468         for (int i = 0; i < cookies.length; i++) {
469             Cookie cookie = cookies[i];
470             if (!(cookie instanceof Cookie2)) {
471                 hasOldStyleCookie = true;
472                 break;
473             }
474             if (cookie.getVersion() > version) {
475                 version = cookie.getVersion();
476             }
477         }
478         if (version < 0) {
479             version = 0;
480         }
481         if (hasOldStyleCookie || version < 1) {
482             // delegate old-style cookie formatting to rfc2109Spec
483             return this.rfc2109.formatCookies(cookies);
484         }
485         // Arrange cookies by path
486         Arrays.sort(cookies, PATH_COMPOARATOR);
487         
488         final StringBuffer buffer = new StringBuffer();
489         // format cookie version
490         this.formatter.format(buffer, new NameValuePair("$Version", Integer.toString(version)));
491         for (int i = 0; i < cookies.length; i++) {
492             buffer.append("; ");
493             Cookie2 cookie = (Cookie2) cookies[i];
494             // format cookie attributes
495             doFormatCookie2(cookie, buffer);
496         }
497         return buffer.toString();
498     }
499 
500     /***
501      * Retrieves valid Port attribute value for the given ports array.
502      * e.g. "8000,8001,8002"
503      *
504      * @param ports int array of ports
505      */
506     private String createPortAttribute(int[] ports) {
507         StringBuffer portValue = new StringBuffer();
508         for (int i = 0, len = ports.length; i < len; i++) {
509             if (i > 0) {
510                 portValue.append(",");
511             }
512             portValue.append(ports[i]);
513         }
514         return portValue.toString();
515     }
516 
517     /***
518      * Parses the given Port attribute value (e.g. "8000,8001,8002")
519      * into an array of ports.
520      *
521      * @param portValue port attribute value
522      * @return parsed array of ports
523      * @throws MalformedCookieException if there is a problem in
524      *          parsing due to invalid portValue.
525      */
526     private int[] parsePortAttribute(final String portValue)
527             throws MalformedCookieException {
528         StringTokenizer st = new StringTokenizer(portValue, ",");
529         int[] ports = new int[st.countTokens()];
530         try {
531             int i = 0;
532             while(st.hasMoreTokens()) {
533                 ports[i] = Integer.parseInt(st.nextToken().trim());
534                 if (ports[i] < 0) {
535                   throw new MalformedCookieException ("Invalid Port attribute.");
536                 }
537                 ++i;
538             }
539         } catch (NumberFormatException e) {
540             throw new MalformedCookieException ("Invalid Port "
541                                                 + "attribute: " + e.getMessage());
542         }
543         return ports;
544     }
545 
546     /***
547      * Gets 'effective host name' as defined in RFC 2965.
548      * <p>
549      * If a host name contains no dots, the effective host name is
550      * that name with the string .local appended to it.  Otherwise
551      * the effective host name is the same as the host name.  Note
552      * that all effective host names contain at least one dot.
553      *
554      * @param host host name where cookie is received from or being sent to.
555      * @return
556      */
557     private static String getEffectiveHost(final String host) {
558         String effectiveHost = host.toLowerCase();
559         if (host.indexOf('.') < 0) {
560             effectiveHost += ".local";
561         }
562         return effectiveHost;
563     }
564 
565     /***
566      * Performs domain-match as defined by the RFC2965.
567      * <p>
568      * Host A's name domain-matches host B's if
569      * <ol>
570      *   <ul>their host name strings string-compare equal; or</ul>
571      *   <ul>A is a HDN string and has the form NB, where N is a non-empty
572      *       name string, B has the form .B', and B' is a HDN string.  (So,
573      *       x.y.com domain-matches .Y.com but not Y.com.)</ul>
574      * </ol>
575      *
576      * @param host host name where cookie is received from or being sent to.
577      * @param domain The cookie domain attribute.
578      * @return true if the specified host matches the given domain.
579      */
580     public boolean domainMatch(String host, String domain) {
581         boolean match = host.equals(domain)
582                         || (domain.startsWith(".") && host.endsWith(domain));
583 
584         return match;
585     }
586 
587     /***
588      * Returns <tt>true</tt> if the given port exists in the given
589      * ports list.
590      *
591      * @param port port of host where cookie was received from or being sent to.
592      * @param ports port list
593      * @return true returns <tt>true</tt> if the given port exists in
594      *         the given ports list; <tt>false</tt> otherwise.
595      */
596     private boolean portMatch(int port, int[] ports) {
597         boolean portInList = false;
598         for (int i = 0, len = ports.length; i < len; i++) {
599             if (port == ports[i]) {
600                 portInList = true;
601                 break;
602             }
603         }
604         return portInList;
605     }
606 
607     /***
608      * <tt>"Path"</tt> attribute handler for RFC 2965 cookie spec.
609      */
610     private class Cookie2PathAttributeHandler
611             implements CookieAttributeHandler {
612 
613         /***
614          * Parse cookie path attribute.
615          */
616         public void parse(final Cookie cookie, final String path)
617                 throws MalformedCookieException {
618             if (cookie == null) {
619                 throw new IllegalArgumentException("Cookie may not be null");
620             }
621             if (path == null) {
622                 throw new MalformedCookieException(
623                         "Missing value for path attribute");
624             }
625             if (path.trim().equals("")) {
626                 throw new MalformedCookieException(
627                         "Blank value for path attribute");
628             }
629             cookie.setPath(path);
630             cookie.setPathAttributeSpecified(true);
631         }
632 
633         /***
634          * Validate cookie path attribute. The value for the Path attribute must be a
635          * prefix of the request-URI (case-sensitive matching).
636          */
637         public void validate(final Cookie cookie, final CookieOrigin origin)
638                 throws MalformedCookieException {
639             if (cookie == null) {
640                 throw new IllegalArgumentException("Cookie may not be null");
641             }
642             if (origin == null) {
643                 throw new IllegalArgumentException("Cookie origin may not be null");
644             }
645             String path = origin.getPath();
646             if (path == null) {
647                 throw new IllegalArgumentException(
648                         "Path of origin host may not be null.");
649             }
650             if (cookie.getPath() == null) {
651                 throw new MalformedCookieException("Invalid cookie state: " +
652                                                    "path attribute is null.");
653             }
654             if (path.trim().equals("")) {
655                 path = PATH_DELIM;
656             }
657 
658             if (!pathMatch(path, cookie.getPath())) {
659                 throw new MalformedCookieException(
660                         "Illegal path attribute \"" + cookie.getPath()
661                         + "\". Path of origin: \"" + path + "\"");
662             }
663         }
664 
665         /***
666          * Match cookie path attribute. The value for the Path attribute must be a
667          * prefix of the request-URI (case-sensitive matching).
668          */
669         public boolean match(final Cookie cookie, final CookieOrigin origin) {
670             if (cookie == null) {
671                 throw new IllegalArgumentException("Cookie may not be null");
672             }
673             if (origin == null) {
674                 throw new IllegalArgumentException("Cookie origin may not be null");
675             }
676             String path = origin.getPath();
677             if (cookie.getPath() == null) {
678                 LOG.warn("Invalid cookie state: path attribute is null.");
679                 return false;
680             }
681             if (path.trim().equals("")) {
682                 path = PATH_DELIM;
683             }
684 
685             if (!pathMatch(path, cookie.getPath())) {
686                 return false;
687             }
688             return true;
689         }
690     }
691 
692     /***
693      * <tt>"Domain"</tt> cookie attribute handler for RFC 2965 cookie spec.
694      */
695     private class Cookie2DomainAttributeHandler
696             implements CookieAttributeHandler {
697 
698         /***
699          * Parse cookie domain attribute.
700          */
701         public void parse(final Cookie cookie, String domain)
702                 throws MalformedCookieException {
703             if (cookie == null) {
704                 throw new IllegalArgumentException("Cookie may not be null");
705             }
706             if (domain == null) {
707                 throw new MalformedCookieException(
708                         "Missing value for domain attribute");
709             }
710             if (domain.trim().equals("")) {
711                 throw new MalformedCookieException(
712                         "Blank value for domain attribute");
713             }
714             domain = domain.toLowerCase();
715             if (!domain.startsWith(".")) {
716                 // Per RFC 2965 section 3.2.2
717                 // "... If an explicitly specified value does not start with
718                 // a dot, the user agent supplies a leading dot ..."
719                 // That effectively implies that the domain attribute 
720                 // MAY NOT be an IP address of a host name
721                 domain = "." + domain;
722             }
723             cookie.setDomain(domain);
724             cookie.setDomainAttributeSpecified(true);
725         }
726 
727         /***
728          * Validate cookie domain attribute.
729          */
730         public void validate(final Cookie cookie, final CookieOrigin origin)
731                 throws MalformedCookieException {
732             if (cookie == null) {
733                 throw new IllegalArgumentException("Cookie may not be null");
734             }
735             if (origin == null) {
736                 throw new IllegalArgumentException("Cookie origin may not be null");
737             }
738             String host = origin.getHost().toLowerCase();
739             if (cookie.getDomain() == null) {
740                 throw new MalformedCookieException("Invalid cookie state: " +
741                                                    "domain not specified");
742             }
743             String cookieDomain = cookie.getDomain().toLowerCase();
744 
745             if (cookie.isDomainAttributeSpecified()) {
746                 // Domain attribute must start with a dot
747                 if (!cookieDomain.startsWith(".")) {
748                     throw new MalformedCookieException("Domain attribute \"" +
749                         cookie.getDomain() + "\" violates RFC 2109: domain must start with a dot");
750                 }
751 
752                 // Domain attribute must contain atleast one embedded dot,
753                 // or the value must be equal to .local.
754                 int dotIndex = cookieDomain.indexOf('.', 1);
755                 if (((dotIndex < 0) || (dotIndex == cookieDomain.length() - 1))
756                     && (!cookieDomain.equals(".local"))) {
757                     throw new MalformedCookieException(
758                             "Domain attribute \"" + cookie.getDomain()
759                             + "\" violates RFC 2965: the value contains no embedded dots "
760                             + "and the value is not .local");
761                 }
762 
763                 // The effective host name must domain-match domain attribute.
764                 if (!domainMatch(host, cookieDomain)) {
765                     throw new MalformedCookieException(
766                             "Domain attribute \"" + cookie.getDomain()
767                             + "\" violates RFC 2965: effective host name does not "
768                             + "domain-match domain attribute.");
769                 }
770 
771                 // effective host name minus domain must not contain any dots
772                 String effectiveHostWithoutDomain = host.substring(
773                         0, host.length() - cookieDomain.length());
774                 if (effectiveHostWithoutDomain.indexOf('.') != -1) {
775                     throw new MalformedCookieException("Domain attribute \""
776                                                        + cookie.getDomain() + "\" violates RFC 2965: "
777                                                        + "effective host minus domain may not contain any dots");
778                 }
779             } else {
780                 // Domain was not specified in header. In this case, domain must
781                 // string match request host (case-insensitive).
782                 if (!cookie.getDomain().equals(host)) {
783                     throw new MalformedCookieException("Illegal domain attribute: \""
784                                                        + cookie.getDomain() + "\"."
785                                                        + "Domain of origin: \""
786                                                        + host + "\"");
787                 }
788             }
789         }
790 
791         /***
792          * Match cookie domain attribute.
793          */
794         public boolean match(final Cookie cookie, final CookieOrigin origin) {
795             if (cookie == null) {
796                 throw new IllegalArgumentException("Cookie may not be null");
797             }
798             if (origin == null) {
799                 throw new IllegalArgumentException("Cookie origin may not be null");
800             }
801             String host = origin.getHost().toLowerCase();
802             String cookieDomain = cookie.getDomain();
803 
804             // The effective host name MUST domain-match the Domain
805             // attribute of the cookie.
806             if (!domainMatch(host, cookieDomain)) {
807                 return false;
808             }
809             // effective host name minus domain must not contain any dots
810             String effectiveHostWithoutDomain = host.substring(
811                     0, host.length() - cookieDomain.length());
812             if (effectiveHostWithoutDomain.indexOf('.') != -1) {
813                 return false;
814             }
815             return true;
816         }
817 
818     }
819 
820     /***
821      * <tt>"Port"</tt> cookie attribute handler for RFC 2965 cookie spec.
822      */
823     private class Cookie2PortAttributeHandler
824             implements CookieAttributeHandler {
825 
826         /***
827          * Parse cookie port attribute.
828          */
829         public void parse(final Cookie cookie, final String portValue)
830                 throws MalformedCookieException {
831             if (cookie == null) {
832                 throw new IllegalArgumentException("Cookie may not be null");
833             }
834             if (cookie instanceof Cookie2) {
835                 Cookie2 cookie2 = (Cookie2) cookie;
836                 if ((portValue == null) || (portValue.trim().equals(""))) {
837                     // If the Port attribute is present but has no value, the
838                     // cookie can only be sent to the request-port.
839                     // Since the default port list contains only request-port, we don't
840                     // need to do anything here.
841                     cookie2.setPortAttributeBlank(true);
842                 } else {
843                     int[] ports = parsePortAttribute(portValue);
844                     cookie2.setPorts(ports);
845                 }
846                 cookie2.setPortAttributeSpecified(true);
847             }
848         }
849 
850         /***
851          * Validate cookie port attribute. If the Port attribute was specified
852          * in header, the request port must be in cookie's port list.
853          */
854         public void validate(final Cookie cookie, final CookieOrigin origin)
855                 throws MalformedCookieException {
856             if (cookie == null) {
857                 throw new IllegalArgumentException("Cookie may not be null");
858             }
859             if (origin == null) {
860                 throw new IllegalArgumentException("Cookie origin may not be null");
861             }
862             if (cookie instanceof Cookie2) {
863                 Cookie2 cookie2 = (Cookie2) cookie;
864                 int port = origin.getPort();
865                 if (cookie2.isPortAttributeSpecified()) {
866                     if (!portMatch(port, cookie2.getPorts())) {
867                         throw new MalformedCookieException(
868                                 "Port attribute violates RFC 2965: "
869                                 + "Request port not found in cookie's port list.");
870                     }
871                 }
872             }
873         }
874 
875         /***
876          * Match cookie port attribute. If the Port attribute is not specified
877          * in header, the cookie can be sent to any port. Otherwise, the request port
878          * must be in the cookie's port list.
879          */
880         public boolean match(final Cookie cookie, final CookieOrigin origin) {
881             if (cookie == null) {
882                 throw new IllegalArgumentException("Cookie may not be null");
883             }
884             if (origin == null) {
885                 throw new IllegalArgumentException("Cookie origin may not be null");
886             }
887             if (cookie instanceof Cookie2) {
888                 Cookie2 cookie2 = (Cookie2) cookie;
889                 int port = origin.getPort();
890                 if (cookie2.isPortAttributeSpecified()) {
891                     if (cookie2.getPorts() == null) {
892                         LOG.warn("Invalid cookie state: port not specified");
893                         return false;
894                     }
895                     if (!portMatch(port, cookie2.getPorts())) {
896                         return false;
897                     }
898                 }
899                 return true;
900             } else {
901                 return false;
902             }
903         }
904     }
905 
906   /***
907    * <tt>"Max-age"</tt> cookie attribute handler for RFC 2965 cookie spec.
908    */
909   private class Cookie2MaxageAttributeHandler
910           implements CookieAttributeHandler {
911 
912       /***
913        * Parse cookie max-age attribute.
914        */
915       public void parse(final Cookie cookie, final String value)
916               throws MalformedCookieException {
917           if (cookie == null) {
918               throw new IllegalArgumentException("Cookie may not be null");
919           }
920           if (value == null) {
921               throw new MalformedCookieException(
922                       "Missing value for max-age attribute");
923           }
924           int age = -1;
925           try {
926               age = Integer.parseInt(value);
927           } catch (NumberFormatException e) {
928               age = -1;
929           }
930           if (age < 0) {
931               throw new MalformedCookieException ("Invalid max-age attribute.");
932           }
933           cookie.setExpiryDate(new Date(System.currentTimeMillis() + age * 1000L));
934       }
935 
936       /***
937        * validate cookie max-age attribute.
938        */
939       public void validate(final Cookie cookie, final CookieOrigin origin) {
940       }
941 
942       /***
943        * @see CookieAttributeHandler#match(org.apache.commons.httpclient.Cookie, String)
944        */
945       public boolean match(final Cookie cookie, final CookieOrigin origin) {
946           return true;
947       }
948 
949   }
950 
951   /***
952    * <tt>"Secure"</tt> cookie attribute handler for RFC 2965 cookie spec.
953    */
954   private class CookieSecureAttributeHandler
955           implements CookieAttributeHandler {
956 
957       public void parse(final Cookie cookie, final String secure)
958               throws MalformedCookieException {
959           cookie.setSecure(true);
960       }
961 
962       public void validate(final Cookie cookie, final CookieOrigin origin)
963               throws MalformedCookieException {
964       }
965 
966       public boolean match(final Cookie cookie, final CookieOrigin origin) {
967           if (cookie == null) {
968               throw new IllegalArgumentException("Cookie may not be null");
969           }
970           if (origin == null) {
971               throw new IllegalArgumentException("Cookie origin may not be null");
972           }
973           return cookie.getSecure() == origin.isSecure();
974       }
975       
976   }
977 
978   /***
979    * <tt>"Commant"</tt> cookie attribute handler for RFC 2965 cookie spec.
980    */
981   private class CookieCommentAttributeHandler
982           implements CookieAttributeHandler {
983 
984       public void parse(final Cookie cookie, final String comment)
985               throws MalformedCookieException {
986           cookie.setComment(comment);
987       }
988 
989       public void validate(final Cookie cookie, final CookieOrigin origin)
990               throws MalformedCookieException {
991       }
992 
993       public boolean match(final Cookie cookie, final CookieOrigin origin) {
994           return true;
995       }
996       
997   }
998 
999   /***
1000    * <tt>"CommantURL"</tt> cookie attribute handler for RFC 2965 cookie spec.
1001    */
1002   private class CookieCommentUrlAttributeHandler
1003           implements CookieAttributeHandler {
1004 
1005       public void parse(final Cookie cookie, final String commenturl)
1006               throws MalformedCookieException {
1007           if (cookie instanceof Cookie2) {
1008               Cookie2 cookie2 = (Cookie2) cookie;
1009               cookie2.setCommentURL(commenturl);
1010           }
1011       }
1012 
1013       public void validate(final Cookie cookie, final CookieOrigin origin)
1014               throws MalformedCookieException {
1015       }
1016 
1017       public boolean match(final Cookie cookie, final CookieOrigin origin) {
1018           return true;
1019       }
1020       
1021   }
1022 
1023   /***
1024    * <tt>"Discard"</tt> cookie attribute handler for RFC 2965 cookie spec.
1025    */
1026   private class CookieDiscardAttributeHandler
1027           implements CookieAttributeHandler {
1028 
1029       public void parse(final Cookie cookie, final String commenturl)
1030               throws MalformedCookieException {
1031           if (cookie instanceof Cookie2) {
1032               Cookie2 cookie2 = (Cookie2) cookie;
1033               cookie2.setDiscard(true);
1034           }
1035       }
1036 
1037       public void validate(final Cookie cookie, final CookieOrigin origin)
1038               throws MalformedCookieException {
1039       }
1040 
1041       public boolean match(final Cookie cookie, final CookieOrigin origin) {
1042           return true;
1043       }
1044       
1045   }
1046 
1047   /***
1048      * <tt>"Version"</tt> cookie attribute handler for RFC 2965 cookie spec.
1049      */
1050     private class Cookie2VersionAttributeHandler
1051             implements CookieAttributeHandler {
1052 
1053         /***
1054          * Parse cookie version attribute.
1055          */
1056         public void parse(final Cookie cookie, final String value)
1057                 throws MalformedCookieException {
1058             if (cookie == null) {
1059                 throw new IllegalArgumentException("Cookie may not be null");
1060             }
1061             if (cookie instanceof Cookie2) {
1062                 Cookie2 cookie2 = (Cookie2) cookie;
1063                 if (value == null) {
1064                     throw new MalformedCookieException(
1065                             "Missing value for version attribute");
1066                 }
1067                 int version = -1;
1068                 try {
1069                     version = Integer.parseInt(value);
1070                 } catch (NumberFormatException e) {
1071                     version = -1;
1072                 }
1073                 if (version < 0) {
1074                     throw new MalformedCookieException("Invalid cookie version.");
1075                 }
1076                 cookie2.setVersion(version);
1077                 cookie2.setVersionAttributeSpecified(true);
1078             }
1079         }
1080 
1081         /***
1082          * validate cookie version attribute. Version attribute is REQUIRED.
1083          */
1084         public void validate(final Cookie cookie, final CookieOrigin origin)
1085                 throws MalformedCookieException {
1086             if (cookie == null) {
1087                 throw new IllegalArgumentException("Cookie may not be null");
1088             }
1089             if (cookie instanceof Cookie2) {
1090                 Cookie2 cookie2 = (Cookie2) cookie;
1091                 if (!cookie2.isVersionAttributeSpecified()) {
1092                     throw new MalformedCookieException(
1093                             "Violates RFC 2965. Version attribute is required.");
1094                 }
1095             }
1096         }
1097 
1098         public boolean match(final Cookie cookie, final CookieOrigin origin) {
1099             return true;
1100         }
1101 
1102     }
1103 
1104     public int getVersion() {
1105         return 1;
1106     }
1107 
1108     public Header getVersionHeader() {
1109         ParameterFormatter formatter = new ParameterFormatter();
1110         StringBuffer buffer = new StringBuffer();
1111         formatter.format(buffer, new NameValuePair("$Version",
1112                 Integer.toString(getVersion())));
1113         return new Header("Cookie2", buffer.toString(), true);
1114     }
1115     
1116 }
1117