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