View Javadoc

1   /*
2    * $HeadURL: https://svn.apache.org/repos/asf/httpcomponents/oac.hc3x/trunk/src/java/org/apache/commons/httpclient/cookie/CookieSpecBase.java $
3    * $Revision: 1425331 $
4    * $Date: 2012-12-22 18:29:41 +0000 (Sat, 22 Dec 2012) $
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.Collection;
34  import java.util.Date;
35  import java.util.LinkedList;
36  import java.util.List;
37  
38  import org.apache.commons.httpclient.Cookie;
39  import org.apache.commons.httpclient.Header;
40  import org.apache.commons.httpclient.HeaderElement;
41  import org.apache.commons.httpclient.NameValuePair;
42  import org.apache.commons.httpclient.util.DateParseException;
43  import org.apache.commons.httpclient.util.DateUtil;
44  import org.apache.commons.logging.Log;
45  import org.apache.commons.logging.LogFactory;
46  
47  /***
48   * 
49   * Cookie management functions shared by all specification.
50   *
51   * @author  B.C. Holmes
52   * @author <a href="mailto:jericho@thinkfree.com">Park, Sung-Gu</a>
53   * @author <a href="mailto:dsale@us.britannica.com">Doug Sale</a>
54   * @author Rod Waldhoff
55   * @author dIon Gillard
56   * @author Sean C. Sullivan
57   * @author <a href="mailto:JEvans@Cyveillance.com">John Evans</a>
58   * @author Marc A. Saegesser
59   * @author <a href="mailto:oleg@ural.ru">Oleg Kalnichevski</a>
60   * @author <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a>
61   * 
62   * @since 2.0 
63   */
64  public class CookieSpecBase implements CookieSpec {
65      
66      /*** Log object */
67      protected static final Log LOG = LogFactory.getLog(CookieSpec.class);
68  
69      /*** Valid date patterns */
70      private Collection datepatterns = null;
71      
72      /*** Default constructor */
73      public CookieSpecBase() {
74          super();
75      }
76  
77  
78      /***
79        * Parses the Set-Cookie value into an array of <tt>Cookie</tt>s.
80        *
81        * <P>The syntax for the Set-Cookie response header is:
82        *
83        * <PRE>
84        * set-cookie      =    "Set-Cookie:" cookies
85        * cookies         =    1#cookie
86        * cookie          =    NAME "=" VALUE * (";" cookie-av)
87        * NAME            =    attr
88        * VALUE           =    value
89        * cookie-av       =    "Comment" "=" value
90        *                 |    "Domain" "=" value
91        *                 |    "Max-Age" "=" value
92        *                 |    "Path" "=" value
93        *                 |    "Secure"
94        *                 |    "Version" "=" 1*DIGIT
95        * </PRE>
96        *
97        * @param host the host from which the <tt>Set-Cookie</tt> value was
98        * received
99        * @param port the port from which the <tt>Set-Cookie</tt> value was
100       * received
101       * @param path the path from which the <tt>Set-Cookie</tt> value was
102       * received
103       * @param secure <tt>true</tt> when the <tt>Set-Cookie</tt> value was
104       * received over secure conection
105       * @param header the <tt>Set-Cookie</tt> received from the server
106       * @return an array of <tt>Cookie</tt>s parsed from the Set-Cookie value
107       * @throws MalformedCookieException if an exception occurs during parsing
108       */
109     public Cookie[] parse(String host, int port, String path, 
110         boolean secure, final String header) 
111         throws MalformedCookieException {
112             
113         LOG.trace("enter CookieSpecBase.parse(" 
114             + "String, port, path, boolean, Header)");
115 
116         if (host == null) {
117             throw new IllegalArgumentException(
118                 "Host of origin may not be null");
119         }
120         if (host.trim().equals("")) {
121             throw new IllegalArgumentException(
122                 "Host of origin may not be blank");
123         }
124         if (port < 0) {
125             throw new IllegalArgumentException("Invalid port: " + port);
126         }
127         if (path == null) {
128             throw new IllegalArgumentException(
129                 "Path of origin may not be null.");
130         }
131         if (header == null) {
132             throw new IllegalArgumentException("Header may not be null.");
133         }
134 
135         if (path.trim().equals("")) {
136             path = PATH_DELIM;
137         }
138         host = host.toLowerCase();
139 
140         String defaultPath = path;    
141         int lastSlashIndex = defaultPath.lastIndexOf(PATH_DELIM);
142         if (lastSlashIndex >= 0) {
143             if (lastSlashIndex == 0) {
144                 //Do not remove the very first slash
145                 lastSlashIndex = 1;
146             }
147             defaultPath = defaultPath.substring(0, lastSlashIndex);
148         }
149 
150         HeaderElement[] headerElements = null;
151 
152         boolean isNetscapeCookie = false; 
153         int i1 = header.toLowerCase().indexOf("expires=");
154         if (i1 != -1) {
155             i1 += "expires=".length();
156             int i2 = header.indexOf(";", i1);
157             if (i2 == -1) {
158                 i2 = header.length(); 
159             }
160             try {
161                 DateUtil.parseDate(header.substring(i1, i2), this.datepatterns);
162                 isNetscapeCookie = true; 
163             } catch (DateParseException e) {
164                 // Does not look like a valid expiry date
165             }
166         }
167         if (isNetscapeCookie) {
168             headerElements = new HeaderElement[] {
169                     new HeaderElement(header.toCharArray())
170             };
171         } else {
172             headerElements = HeaderElement.parseElements(header.toCharArray());
173         }
174         
175         Cookie[] cookies = new Cookie[headerElements.length];
176 
177         for (int i = 0; i < headerElements.length; i++) {
178 
179             HeaderElement headerelement = headerElements[i];
180             Cookie cookie = null;
181             try {
182                 cookie = new Cookie(host,
183                                     headerelement.getName(),
184                                     headerelement.getValue(),
185                                     defaultPath, 
186                                     null,
187                                     false);
188             } catch (IllegalArgumentException e) {
189                 throw new MalformedCookieException(e.getMessage()); 
190             }
191             // cycle through the parameters
192             NameValuePair[] parameters = headerelement.getParameters();
193             // could be null. In case only a header element and no parameters.
194             if (parameters != null) {
195 
196                 for (int j = 0; j < parameters.length; j++) {
197                     parseAttribute(parameters[j], cookie);
198                 }
199             }
200             cookies[i] = cookie;
201         }
202         return cookies;
203     }
204 
205 
206     /***
207       * Parse the <tt>"Set-Cookie"</tt> {@link Header} into an array of {@link
208       * Cookie}s.
209       *
210       * <P>The syntax for the Set-Cookie response header is:
211       *
212       * <PRE>
213       * set-cookie      =    "Set-Cookie:" cookies
214       * cookies         =    1#cookie
215       * cookie          =    NAME "=" VALUE * (";" cookie-av)
216       * NAME            =    attr
217       * VALUE           =    value
218       * cookie-av       =    "Comment" "=" value
219       *                 |    "Domain" "=" value
220       *                 |    "Max-Age" "=" value
221       *                 |    "Path" "=" value
222       *                 |    "Secure"
223       *                 |    "Version" "=" 1*DIGIT
224       * </PRE>
225       *
226       * @param host the host from which the <tt>Set-Cookie</tt> header was
227       * received
228       * @param port the port from which the <tt>Set-Cookie</tt> header was
229       * received
230       * @param path the path from which the <tt>Set-Cookie</tt> header was
231       * received
232       * @param secure <tt>true</tt> when the <tt>Set-Cookie</tt> header was
233       * received over secure conection
234       * @param header the <tt>Set-Cookie</tt> received from the server
235       * @return an array of <tt>Cookie</tt>s parsed from the <tt>"Set-Cookie"
236       * </tt> header
237       * @throws MalformedCookieException if an exception occurs during parsing
238       */
239     public Cookie[] parse(
240         String host, int port, String path, boolean secure, final Header header)
241         throws MalformedCookieException {
242             
243         LOG.trace("enter CookieSpecBase.parse("
244             + "String, port, path, boolean, String)");
245         if (header == null) {
246             throw new IllegalArgumentException("Header may not be null.");
247         }
248         return parse(host, port, path, secure, header.getValue());
249     }
250 
251 
252     /***
253       * Parse the cookie attribute and update the corresponsing {@link Cookie}
254       * properties.
255       *
256       * @param attribute {@link HeaderElement} cookie attribute from the
257       * <tt>Set- Cookie</tt>
258       * @param cookie {@link Cookie} to be updated
259       * @throws MalformedCookieException if an exception occurs during parsing
260       */
261 
262     public void parseAttribute(
263         final NameValuePair attribute, final Cookie cookie)
264         throws MalformedCookieException {
265             
266         if (attribute == null) {
267             throw new IllegalArgumentException("Attribute may not be null.");
268         }
269         if (cookie == null) {
270             throw new IllegalArgumentException("Cookie may not be null.");
271         }
272         final String paramName = attribute.getName().toLowerCase();
273         String paramValue = attribute.getValue();
274 
275         if (paramName.equals("path")) {
276 
277             if ((paramValue == null) || (paramValue.trim().equals(""))) {
278                 paramValue = "/";
279             }
280             cookie.setPath(paramValue);
281             cookie.setPathAttributeSpecified(true);
282 
283         } else if (paramName.equals("domain")) {
284 
285             if (paramValue == null) {
286                 throw new MalformedCookieException(
287                     "Missing value for domain attribute");
288             }
289             if (paramValue.trim().equals("")) {
290                 throw new MalformedCookieException(
291                     "Blank value for domain attribute");
292             }
293             cookie.setDomain(paramValue);
294             cookie.setDomainAttributeSpecified(true);
295 
296         } else if (paramName.equals("max-age")) {
297 
298             if (paramValue == null) {
299                 throw new MalformedCookieException(
300                     "Missing value for max-age attribute");
301             }
302             int age;
303             try {
304                 age = Integer.parseInt(paramValue);
305             } catch (NumberFormatException e) {
306                 throw new MalformedCookieException ("Invalid max-age "
307                     + "attribute: " + e.getMessage());
308             }
309             cookie.setExpiryDate(
310                 new Date(System.currentTimeMillis() + age * 1000L));
311 
312         } else if (paramName.equals("secure")) {
313 
314             cookie.setSecure(true);
315 
316         } else if (paramName.equals("comment")) {
317 
318             cookie.setComment(paramValue);
319 
320         } else if (paramName.equals("expires")) {
321 
322             if (paramValue == null) {
323                 throw new MalformedCookieException(
324                     "Missing value for expires attribute");
325             }
326 
327             try {
328                 cookie.setExpiryDate(DateUtil.parseDate(paramValue, this.datepatterns));
329             } catch (DateParseException dpe) {
330                 LOG.debug("Error parsing cookie date", dpe);
331                 throw new MalformedCookieException(
332                     "Unable to parse expiration date parameter: " 
333                     + paramValue);
334             }
335         } else {
336             if (LOG.isDebugEnabled()) {
337                 LOG.debug("Unrecognized cookie attribute: " 
338                     + attribute.toString());
339             }
340         }
341     }
342 
343     
344 	public Collection getValidDateFormats() {
345 		return this.datepatterns;
346 	}
347 
348 	public void setValidDateFormats(final Collection datepatterns) {
349 		this.datepatterns = datepatterns;
350 	}
351 
352 	/***
353       * Performs most common {@link Cookie} validation
354       *
355       * @param host the host from which the {@link Cookie} was received
356       * @param port the port from which the {@link Cookie} was received
357       * @param path the path from which the {@link Cookie} was received
358       * @param secure <tt>true</tt> when the {@link Cookie} was received using a
359       * secure connection
360       * @param cookie The cookie to validate.
361       * @throws MalformedCookieException if an exception occurs during
362       * validation
363       */
364     
365     public void validate(String host, int port, String path, 
366         boolean secure, final Cookie cookie) 
367         throws MalformedCookieException {
368             
369         LOG.trace("enter CookieSpecBase.validate("
370             + "String, port, path, boolean, Cookie)");
371         if (host == null) {
372             throw new IllegalArgumentException(
373                 "Host of origin may not be null");
374         }
375         if (host.trim().equals("")) {
376             throw new IllegalArgumentException(
377                 "Host of origin may not be blank");
378         }
379         if (port < 0) {
380             throw new IllegalArgumentException("Invalid port: " + port);
381         }
382         if (path == null) {
383             throw new IllegalArgumentException(
384                 "Path of origin may not be null.");
385         }
386         if (path.trim().equals("")) {
387             path = PATH_DELIM;
388         }
389         host = host.toLowerCase();
390         // check version
391         if (cookie.getVersion() < 0) {
392             throw new MalformedCookieException ("Illegal version number " 
393                 + cookie.getValue());
394         }
395 
396         // security check... we musn't allow the server to give us an
397         // invalid domain scope
398 
399         // Validate the cookies domain attribute.  NOTE:  Domains without 
400         // any dots are allowed to support hosts on private LANs that don't 
401         // have DNS names.  Since they have no dots, to domain-match the 
402         // request-host and domain must be identical for the cookie to sent 
403         // back to the origin-server.
404         if (host.indexOf(".") >= 0) {
405             // Not required to have at least two dots.  RFC 2965.
406             // A Set-Cookie2 with Domain=ajax.com will be accepted.
407 
408             // domain must match host
409             if (!host.endsWith(cookie.getDomain())) {
410                 String s = cookie.getDomain();
411                 if (s.startsWith(".")) {
412                     s = s.substring(1, s.length());
413                 }
414                 if (!host.equals(s)) { 
415                     throw new MalformedCookieException(
416                         "Illegal domain attribute \"" + cookie.getDomain() 
417                         + "\". Domain of origin: \"" + host + "\"");
418                 }
419             }
420         } else {
421             if (!host.equals(cookie.getDomain())) {
422                 throw new MalformedCookieException(
423                     "Illegal domain attribute \"" + cookie.getDomain() 
424                     + "\". Domain of origin: \"" + host + "\"");
425             }
426         }
427 
428         // another security check... we musn't allow the server to give us a
429         // cookie that doesn't match this path
430 
431         if (!path.startsWith(cookie.getPath())) {
432             throw new MalformedCookieException(
433                 "Illegal path attribute \"" + cookie.getPath() 
434                 + "\". Path of origin: \"" + path + "\"");
435         }
436     }
437 
438 
439     /***
440      * Return <tt>true</tt> if the cookie should be submitted with a request
441      * with given attributes, <tt>false</tt> otherwise.
442      * @param host the host to which the request is being submitted
443      * @param port the port to which the request is being submitted (ignored)
444      * @param path the path to which the request is being submitted
445      * @param secure <tt>true</tt> if the request is using a secure connection
446      * @param cookie {@link Cookie} to be matched
447      * @return true if the cookie matches the criterium
448      */
449 
450     public boolean match(String host, int port, String path, 
451         boolean secure, final Cookie cookie) {
452             
453         LOG.trace("enter CookieSpecBase.match("
454             + "String, int, String, boolean, Cookie");
455             
456         if (host == null) {
457             throw new IllegalArgumentException(
458                 "Host of origin may not be null");
459         }
460         if (host.trim().equals("")) {
461             throw new IllegalArgumentException(
462                 "Host of origin may not be blank");
463         }
464         if (port < 0) {
465             throw new IllegalArgumentException("Invalid port: " + port);
466         }
467         if (path == null) {
468             throw new IllegalArgumentException(
469                 "Path of origin may not be null.");
470         }
471         if (cookie == null) {
472             throw new IllegalArgumentException("Cookie may not be null");
473         }
474         if (path.trim().equals("")) {
475             path = PATH_DELIM;
476         }
477         host = host.toLowerCase();
478         if (cookie.getDomain() == null) {
479             LOG.warn("Invalid cookie state: domain not specified");
480             return false;
481         }
482         if (cookie.getPath() == null) {
483             LOG.warn("Invalid cookie state: path not specified");
484             return false;
485         }
486         
487         return
488             // only add the cookie if it hasn't yet expired 
489             (cookie.getExpiryDate() == null 
490                 || cookie.getExpiryDate().after(new Date()))
491             // and the domain pattern matches 
492             && (domainMatch(host, cookie.getDomain()))
493             // and the path is null or matching
494             && (pathMatch(path, cookie.getPath()))
495             // and if the secure flag is set, only if the request is 
496             // actually secure 
497             && (cookie.getSecure() ? secure : true);      
498     }
499 
500     /***
501      * Performs domain-match as implemented in common browsers.
502      * @param host The target host.
503      * @param domain The cookie domain attribute.
504      * @return true if the specified host matches the given domain.
505      */
506     public boolean domainMatch(final String host, String domain) {
507         if (host.equals(domain)) {
508             return true;
509         }
510         if (!domain.startsWith(".")) {
511             domain = "." + domain;
512         }
513         return host.endsWith(domain) || host.equals(domain.substring(1));
514     }
515 
516     /***
517      * Performs path-match as implemented in common browsers.
518      * @param path The target path.
519      * @param topmostPath The cookie path attribute.
520      * @return true if the paths match
521      */
522     public boolean pathMatch(final String path, final String topmostPath) {
523         boolean match = path.startsWith (topmostPath);
524         // if there is a match and these values are not exactly the same we have
525         // to make sure we're not matcing "/foobar" and "/foo"
526         if (match && path.length() != topmostPath.length()) {
527             if (!topmostPath.endsWith(PATH_DELIM)) {
528                 match = (path.charAt(topmostPath.length()) == PATH_DELIM_CHAR);
529             }
530         }
531         return match;
532     }
533 
534     /***
535      * Return an array of {@link Cookie}s that should be submitted with a
536      * request with given attributes, <tt>false</tt> otherwise.
537      * @param host the host to which the request is being submitted
538      * @param port the port to which the request is being submitted (currently
539      * ignored)
540      * @param path the path to which the request is being submitted
541      * @param secure <tt>true</tt> if the request is using a secure protocol
542      * @param cookies an array of <tt>Cookie</tt>s to be matched
543      * @return an array of <tt>Cookie</tt>s matching the criterium
544      */
545 
546     public Cookie[] match(String host, int port, String path, 
547         boolean secure, final Cookie cookies[]) {
548             
549         LOG.trace("enter CookieSpecBase.match("
550             + "String, int, String, boolean, Cookie[])");
551 
552         if (cookies == null) {
553             return null;
554         }
555         List matching = new LinkedList();
556         for (int i = 0; i < cookies.length; i++) {
557             if (match(host, port, path, secure, cookies[i])) {
558                 addInPathOrder(matching, cookies[i]);
559             }
560         }
561         return (Cookie[]) matching.toArray(new Cookie[matching.size()]);
562     }
563 
564 
565     /***
566      * Adds the given cookie into the given list in descending path order. That
567      * is, more specific path to least specific paths.  This may not be the
568      * fastest algorythm, but it'll work OK for the small number of cookies
569      * we're generally dealing with.
570      *
571      * @param list - the list to add the cookie to
572      * @param addCookie - the Cookie to add to list
573      */
574     private static void addInPathOrder(List list, Cookie addCookie) {
575         int i = 0;
576 
577         for (i = 0; i < list.size(); i++) {
578             Cookie c = (Cookie) list.get(i);
579             if (addCookie.compare(addCookie, c) > 0) {
580                 break;
581             }
582         }
583         list.add(i, addCookie);
584     }
585 
586     /***
587      * Return a string suitable for sending in a <tt>"Cookie"</tt> header
588      * @param cookie a {@link Cookie} to be formatted as string
589      * @return a string suitable for sending in a <tt>"Cookie"</tt> header.
590      */
591     public String formatCookie(Cookie cookie) {
592         LOG.trace("enter CookieSpecBase.formatCookie(Cookie)");
593         if (cookie == null) {
594             throw new IllegalArgumentException("Cookie may not be null");
595         }
596         StringBuffer buf = new StringBuffer();
597         buf.append(cookie.getName());
598         buf.append("=");
599         String s = cookie.getValue();
600         if (s != null) {
601             buf.append(s);
602         }
603         return buf.toString();
604     }
605 
606     /***
607      * Create a <tt>"Cookie"</tt> header value containing all {@link Cookie}s in
608      * <i>cookies</i> suitable for sending in a <tt>"Cookie"</tt> header
609      * @param cookies an array of {@link Cookie}s to be formatted
610      * @return a string suitable for sending in a Cookie header.
611      * @throws IllegalArgumentException if an input parameter is illegal
612      */
613 
614     public String formatCookies(Cookie[] cookies)
615       throws IllegalArgumentException {
616         LOG.trace("enter CookieSpecBase.formatCookies(Cookie[])");
617         if (cookies == null) {
618             throw new IllegalArgumentException("Cookie array may not be null");
619         }
620         if (cookies.length == 0) {
621             throw new IllegalArgumentException("Cookie array may not be empty");
622         }
623 
624         StringBuffer buffer = new StringBuffer();
625         for (int i = 0; i < cookies.length; i++) {
626             if (i > 0) {
627                 buffer.append("; ");
628             }
629             buffer.append(formatCookie(cookies[i]));
630         }
631         return buffer.toString();
632     }
633 
634 
635     /***
636      * Create a <tt>"Cookie"</tt> {@link Header} containing all {@link Cookie}s
637      * in <i>cookies</i>.
638      * @param cookies an array of {@link Cookie}s to be formatted as a <tt>"
639      * Cookie"</tt> header
640      * @return a <tt>"Cookie"</tt> {@link Header}.
641      */
642     public Header formatCookieHeader(Cookie[] cookies) {
643         LOG.trace("enter CookieSpecBase.formatCookieHeader(Cookie[])");
644         return new Header("Cookie", formatCookies(cookies));
645     }
646 
647 
648     /***
649      * Create a <tt>"Cookie"</tt> {@link Header} containing the {@link Cookie}.
650      * @param cookie <tt>Cookie</tt>s to be formatted as a <tt>Cookie</tt>
651      * header
652      * @return a Cookie header.
653      */
654     public Header formatCookieHeader(Cookie cookie) {
655         LOG.trace("enter CookieSpecBase.formatCookieHeader(Cookie)");
656         return new Header("Cookie", formatCookie(cookie));
657     }
658 
659 }