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