This project has retired. For details please refer to its Attic page.
CmisHttpCookie xref

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   * http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  /*
20   * This class has been taken from Apache Harmony (http://harmony.apache.org/) 
21   * and has been modified to work with OpenCMIS.
22   */
23  package org.apache.chemistry.opencmis.client.bindings.spi.cookies;
24  
25  import java.io.Serializable;
26  import java.util.ArrayList;
27  import java.util.Date;
28  import java.util.HashMap;
29  import java.util.List;
30  import java.util.Locale;
31  import java.util.regex.Matcher;
32  import java.util.regex.Pattern;
33  
34  /**
35   * This class represents a http cookie, which indicates the status information
36   * between the client agent side and the server side. According to RFC, there
37   * are 3 http cookie specifications. This class is compatible with all the three
38   * forms. HttpCookie class can accept all these 3 forms of syntax.
39   */
40  public final class CmisHttpCookie implements Cloneable, Serializable {
41  
42      private static final long serialVersionUID = 1L;
43  
44      private static final String DOT_STR = ".";
45      private static final String LOCAL_STR = ".local";
46      private static final String QUOTE_STR = "\"";
47      private static final String COMMA_STR = ",";
48      private static Pattern HEAD_PATTERN = Pattern.compile("Set-Cookie2?:", Pattern.CASE_INSENSITIVE);
49      private static Pattern NAME_PATTERN = Pattern.compile(
50              "([^$=,\u0085\u2028\u2029][^,\n\t\r\r\n\u0085\u2028\u2029]*?)=([^;]*)(;)?", Pattern.DOTALL
51                      | Pattern.CASE_INSENSITIVE);
52      private static Pattern ATTR_PATTERN0 = Pattern.compile("([^;=]*)(?:=([^;]*))?");
53      private static Pattern ATTR_PATTERN1 = Pattern.compile("(,?[^;=]*)(?:=([^;,]*))?((?=.))?");
54  
55      private abstract static class Setter {
56          boolean set;
57  
58          Setter() {
59              set = false;
60          }
61  
62          boolean isSet() {
63              return set;
64          }
65  
66          void set(boolean isSet) {
67              set = isSet;
68          }
69  
70          abstract void setValue(String value, CmisHttpCookie cookie);
71  
72          void validate(String value, CmisHttpCookie cookie) {
73              if (cookie.getVersion() == 1 && value != null && value.contains(COMMA_STR)) {
74                  throw new IllegalArgumentException();
75              }
76          }
77      }
78  
79      private HashMap<String, Setter> attributeSet = new HashMap<String, Setter>();
80  
81      /**
82       * A utility method used to check whether the host name is in a domain or
83       * not.
84       * 
85       * @param domain
86       *            the domain to be checked against
87       * @param host
88       *            the host to be checked
89       * @return true if the host is in the domain, false otherwise
90       */
91      public static boolean domainMatches(String domain, String host) {
92          if (domain == null || host == null) {
93              return false;
94          }
95          String newDomain = domain.toLowerCase();
96          String newHost = host.toLowerCase();
97          return newDomain.equals(newHost)
98                  || (isValidDomain(newDomain) && effDomainMatches(newDomain, newHost) && isValidHost(newDomain, newHost));
99      }
100 
101     private static boolean effDomainMatches(String domain, String host) {
102         // calculate effective host name
103         String effHost = host.indexOf(DOT_STR) != -1 ? host : (host + LOCAL_STR);
104 
105         // Rule 2: domain and host are string-compare equal, or A = NB, B = .B'
106         // and N is a non-empty name string
107         boolean inDomain = domain.equals(effHost);
108         inDomain = inDomain
109                 || (effHost.endsWith(domain) && effHost.length() > domain.length() && domain.startsWith(DOT_STR));
110 
111         return inDomain;
112     }
113 
114     private static boolean isCommaDelim(CmisHttpCookie cookie) {
115         String value = cookie.getValue();
116         if (value.startsWith(QUOTE_STR) && value.endsWith(QUOTE_STR)) {
117             cookie.setValue(value.substring(1, value.length() - 1));
118             return false;
119         }
120 
121         if (cookie.getVersion() == 1 && value.contains(COMMA_STR)) {
122             cookie.setValue(value.substring(0, value.indexOf(COMMA_STR)));
123             return true;
124         }
125 
126         return false;
127     }
128 
129     private static boolean isValidDomain(String domain) {
130         // Rule 1: The value for Domain contains embedded dots, or is .local
131         if (domain.length() <= 2) {
132             return false;
133         }
134 
135         return domain.substring(1, domain.length() - 1).indexOf(DOT_STR) != -1 || domain.equals(LOCAL_STR);
136     }
137 
138     private static boolean isValidHost(String domain, String host) {
139         // Rule 3: host does not end with domain, or the remainder does not
140         // contain "."
141         boolean matches = !host.endsWith(domain);
142         if (!matches) {
143             String hostSub = host.substring(0, host.length() - domain.length());
144             matches = hostSub.indexOf(DOT_STR) == -1;
145         }
146 
147         return matches;
148     }
149 
150     /**
151      * Constructs a cookie from a string. The string should comply with
152      * set-cookie or set-cookie2 header format as specified in RFC 2965. Since
153      * set-cookies2 syntax allows more than one cookie definitions in one
154      * header, the returned object is a list.
155      * 
156      * @param header
157      *            a set-cookie or set-cookie2 header.
158      * @return a list of constructed cookies
159      * @throws IllegalArgumentException
160      *             if the string does not comply with cookie specification, or
161      *             the cookie name contains illegal characters, or reserved
162      *             tokens of cookie specification appears
163      * @throws NullPointerException
164      *             if header is null
165      */
166     public static List<CmisHttpCookie> parse(String header) {
167         Matcher matcher = HEAD_PATTERN.matcher(header);
168         // Parse cookie name & value
169         List<CmisHttpCookie> list = null;
170         CmisHttpCookie cookie = null;
171         String headerString = header;
172         int version = 0;
173         // process set-cookie | set-cookie2 head
174         if (matcher.find()) {
175             String cookieHead = matcher.group();
176             if ("set-cookie2:".equalsIgnoreCase(cookieHead)) {
177                 version = 1;
178             }
179             headerString = header.substring(cookieHead.length());
180         }
181 
182         // parse cookie name/value pair
183         matcher = NAME_PATTERN.matcher(headerString);
184         if (matcher.lookingAt()) {
185             list = new ArrayList<CmisHttpCookie>();
186             cookie = new CmisHttpCookie(matcher.group(1), matcher.group(2));
187             cookie.setVersion(version);
188 
189             /*
190              * Comma is a delimiter in cookie spec 1.1. If find comma in version
191              * 1 cookie header, part of matched string need to be spitted out.
192              */
193             String nameGroup = matcher.group();
194             if (isCommaDelim(cookie)) {
195                 headerString = headerString.substring(nameGroup.indexOf(COMMA_STR));
196             } else {
197                 headerString = headerString.substring(nameGroup.length());
198             }
199             list.add(cookie);
200         } else {
201             throw new IllegalArgumentException();
202         }
203 
204         // parse cookie headerString
205         while (!(headerString.length() == 0)) {
206             matcher = cookie.getVersion() == 1 ? ATTR_PATTERN1.matcher(headerString) : ATTR_PATTERN0
207                     .matcher(headerString);
208 
209             if (matcher.lookingAt()) {
210                 String attrName = matcher.group(1).trim();
211 
212                 // handle special situation like: <..>;;<..>
213                 if (attrName.length() == 0) {
214                     headerString = headerString.substring(1);
215                     continue;
216                 }
217 
218                 // If port is the attribute, then comma will not be used as a
219                 // delimiter
220                 if (attrName.equalsIgnoreCase("port") || attrName.equalsIgnoreCase("expires")) {
221                     int start = matcher.regionStart();
222                     matcher = ATTR_PATTERN0.matcher(headerString);
223                     matcher.region(start, headerString.length());
224                     matcher.lookingAt();
225                 } else if (cookie.getVersion() == 1 && attrName.startsWith(COMMA_STR)) {
226                     // If the last encountered token is comma, and the parsed
227                     // attribute is not port, then this attribute/value pair
228                     // ends.
229                     headerString = headerString.substring(1);
230                     matcher = NAME_PATTERN.matcher(headerString);
231                     if (matcher.lookingAt()) {
232                         cookie = new CmisHttpCookie(matcher.group(1), matcher.group(2));
233                         list.add(cookie);
234                         headerString = headerString.substring(matcher.group().length());
235                         continue;
236                     }
237                 }
238 
239                 Setter setter = cookie.attributeSet.get(attrName.toLowerCase());
240                 if (setter != null && !setter.isSet()) {
241                     String attrValue = matcher.group(2);
242                     setter.validate(attrValue, cookie);
243                     setter.setValue(matcher.group(2), cookie);
244                 }
245                 headerString = headerString.substring(matcher.end());
246             }
247         }
248 
249         return list;
250     }
251 
252     private String comment;
253     private String commentURL;
254     private boolean discard;
255     private String domain;
256     private long maxAge = -1l;
257     private String name;
258     private String path;
259     private String portList;
260     private boolean secure;
261     private String value;
262     private int version = 1;
263 
264     {
265         attributeSet.put("comment", new Setter() {
266             @Override
267             void setValue(String value, CmisHttpCookie cookie) {
268                 cookie.setComment(value);
269                 if (cookie.getComment() != null) {
270                     set(true);
271                 }
272             }
273         });
274         attributeSet.put("commenturl", new Setter() {
275             @Override
276             void setValue(String value, CmisHttpCookie cookie) {
277                 cookie.setCommentURL(value);
278                 if (cookie.getCommentURL() != null) {
279                     set(true);
280                 }
281             }
282         });
283         attributeSet.put("discard", new Setter() {
284             @Override
285             void setValue(String value, CmisHttpCookie cookie) {
286                 cookie.setDiscard(true);
287                 set(true);
288             }
289         });
290         attributeSet.put("domain", new Setter() {
291             @Override
292             void setValue(String value, CmisHttpCookie cookie) {
293                 cookie.setDomain(value);
294                 if (cookie.getDomain() != null) {
295                     set(true);
296                 }
297             }
298         });
299         attributeSet.put("max-age", new Setter() {
300             @Override
301             void setValue(String value, CmisHttpCookie cookie) {
302                 try {
303                     cookie.setMaxAge(Long.parseLong(value));
304                 } catch (NumberFormatException e) {
305                     throw new IllegalArgumentException("Invalid max-age!");
306                 }
307                 set(true);
308 
309                 if (!attributeSet.get("version").isSet()) {
310                     cookie.setVersion(1);
311                 }
312             }
313         });
314 
315         attributeSet.put("path", new Setter() {
316             @Override
317             void setValue(String value, CmisHttpCookie cookie) {
318                 cookie.setPath(value);
319                 if (cookie.getPath() != null) {
320                     set(true);
321                 }
322             }
323         });
324         attributeSet.put("port", new Setter() {
325             @Override
326             void setValue(String value, CmisHttpCookie cookie) {
327                 cookie.setPortlist(value);
328                 if (cookie.getPortlist() != null) {
329                     set(true);
330                 }
331             }
332 
333             @Override
334             void validate(String v, CmisHttpCookie cookie) {
335                 return;
336             }
337         });
338         attributeSet.put("secure", new Setter() {
339             @Override
340             void setValue(String value, CmisHttpCookie cookie) {
341                 cookie.setSecure(true);
342                 set(true);
343             }
344         });
345         attributeSet.put("version", new Setter() {
346             @Override
347             void setValue(String value, CmisHttpCookie cookie) {
348                 try {
349                     int v = Integer.parseInt(value);
350                     if (v > cookie.getVersion()) {
351                         cookie.setVersion(v);
352                     }
353                 } catch (NumberFormatException e) {
354                     throw new IllegalArgumentException("Invalid version!");
355                 }
356                 if (cookie.getVersion() != 0) {
357                     set(true);
358                 }
359             }
360         });
361 
362         attributeSet.put("expires", new Setter() {
363             @SuppressWarnings("deprecation")
364             @Override
365             void setValue(String value, CmisHttpCookie cookie) {
366                 cookie.setVersion(0);
367                 attributeSet.get("version").set(true);
368                 if (!attributeSet.get("max-age").isSet()) {
369                     attributeSet.get("max-age").set(true);
370                     if (!"en".equalsIgnoreCase(Locale.getDefault().getLanguage())) {
371                         cookie.setMaxAge(0);
372                         return;
373                     }
374                     try {
375                         cookie.setMaxAge((Date.parse(value) - System.currentTimeMillis()) / 1000);
376                     } catch (IllegalArgumentException e) {
377                         cookie.setMaxAge(0);
378                     }
379                 }
380             }
381 
382             @Override
383             void validate(String v, CmisHttpCookie cookie) {
384                 return;
385             }
386         });
387     }
388 
389     /**
390      * Initializes a cookie with the specified name and value.
391      * 
392      * The name attribute can just contain ASCII characters, which is immutable
393      * after creation. Commas, white space and semicolons are not allowed. The $
394      * character is also not allowed to be the beginning of the name.
395      * 
396      * The value attribute depends on what the server side is interested. The
397      * setValue method can be used to change it.
398      * 
399      * RFC 2965 is the default cookie specification of this class. If one wants
400      * to change the version of the cookie, the setVersion method is available.
401      * 
402      * @param name
403      *            - the specific name of the cookie
404      * @param value
405      *            - the specific value of the cookie
406      * 
407      * @throws IllegalArgumentException
408      *             - if the name contains not-allowed or reserved characters
409      * 
410      * @throws NullPointerException
411      *             if the value of name is null
412      */
413     public CmisHttpCookie(String name, String value) {
414         String ntrim = name.trim(); // erase leading and trailing whitespaces
415         if (!isValidName(ntrim)) {
416             throw new IllegalArgumentException("Invalid name!");
417         }
418 
419         this.name = ntrim;
420         this.value = value;
421     }
422 
423     private void attrToString(StringBuilder builder, String attrName, String attrValue) {
424         if (attrValue != null && builder != null) {
425             builder.append(";");
426             builder.append("$");
427             builder.append(attrName);
428             builder.append("=\"");
429             builder.append(attrValue);
430             builder.append(QUOTE_STR);
431         }
432     }
433 
434     /**
435      * Answers a copy of this object.
436      * 
437      * @return a copy of this cookie
438      */
439     @Override
440     public Object clone() {
441         try {
442             CmisHttpCookie obj = (CmisHttpCookie) super.clone();
443             return obj;
444         } catch (CloneNotSupportedException e) {
445             return null;
446         }
447     }
448 
449     /**
450      * Answers whether two cookies are equal. Two cookies are equal if they have
451      * the same domain and name in a case-insensitive mode and path in a
452      * case-sensitive mode.
453      * 
454      * @param obj
455      *            the object to be compared.
456      * @return true if two cookies equals, false otherwise
457      */
458     @Override
459     public boolean equals(Object obj) {
460         if (obj == this) {
461             return true;
462         }
463         if (obj instanceof CmisHttpCookie) {
464             CmisHttpCookie anotherCookie = (CmisHttpCookie) obj;
465             if (name.equalsIgnoreCase(anotherCookie.getName())) {
466                 String anotherDomain = anotherCookie.getDomain();
467                 boolean equals = domain == null ? anotherDomain == null : domain.equalsIgnoreCase(anotherDomain);
468                 if (equals) {
469                     String anotherPath = anotherCookie.getPath();
470                     return path == null ? anotherPath == null : path.equals(anotherPath);
471                 }
472             }
473         }
474         return false;
475     }
476 
477     /**
478      * Answers the value of comment attribute(specified in RFC 2965) of this
479      * cookie.
480      * 
481      * @return the value of comment attribute
482      */
483     public String getComment() {
484         return comment;
485     }
486 
487     /**
488      * Answers the value of commentURL attribute(specified in RFC 2965) of this
489      * cookie.
490      * 
491      * @return the value of commentURL attribute
492      */
493     public String getCommentURL() {
494         return commentURL;
495     }
496 
497     /**
498      * Answers the value of discard attribute(specified in RFC 2965) of this
499      * cookie.
500      * 
501      * @return discard value of this cookie
502      */
503     public boolean getDiscard() {
504         return discard;
505     }
506 
507     /**
508      * Answers the domain name for this cookie in the format specified in RFC
509      * 2965
510      * 
511      * @return the domain value of this cookie
512      */
513     public String getDomain() {
514         return domain;
515     }
516 
517     /**
518      * Returns the Max-Age value as specified in RFC 2965 of this cookie.
519      * 
520      * @return the Max-Age value
521      */
522     public long getMaxAge() {
523         return maxAge;
524     }
525 
526     /**
527      * Answers the name for this cookie.
528      * 
529      * @return the name for this cookie
530      */
531     public String getName() {
532         return name;
533     }
534 
535     /**
536      * Answers the path part of a request URL to which this cookie is returned.
537      * This cookie is visible to all subpaths.
538      * 
539      * @return the path used to return the cookie
540      */
541     public String getPath() {
542         return path;
543     }
544 
545     /**
546      * Answers the value of port attribute(specified in RFC 2965) of this
547      * cookie.
548      * 
549      * @return port list of this cookie
550      */
551     public String getPortlist() {
552         return portList;
553     }
554 
555     /**
556      * Answers true if the browser only sends cookies over a secure protocol.
557      * False if can send cookies through any protocols.
558      * 
559      * @return true if sends cookies only through secure protocol, false
560      *         otherwise
561      */
562     public boolean getSecure() {
563         return secure;
564     }
565 
566     /**
567      * Answers the value of this cookie.
568      * 
569      * @return the value of this cookie
570      */
571     public String getValue() {
572         return value;
573     }
574 
575     /**
576      * Get the version of this cookie
577      * 
578      * @return 0 indicates the original Netscape cookie specification, while 1
579      *         indicates RFC 2965/2109 specification.
580      */
581     public int getVersion() {
582         return version;
583     }
584 
585     /**
586      * Answers whether the cookie has expired.
587      * 
588      * @return true is the cookie has expired, false otherwise
589      */
590     public boolean hasExpired() {
591         // -1 indicates the cookie will persist until browser shutdown
592         // so the cookie is not expired.
593         if (maxAge == -1l) {
594             return false;
595         }
596 
597         boolean expired = false;
598         if (maxAge <= 0l) {
599             expired = true;
600         }
601         return expired;
602     }
603 
604     /**
605      * Answers hash code of this http cookie. The result is calculated as below:
606      * 
607      * getName().toLowerCase().hashCode() + getDomain().toLowerCase().hashCode()
608      * + getPath().hashCode()
609      * 
610      * @return the hash code of this cookie
611      */
612     @Override
613     public int hashCode() {
614         int hashCode = name.toLowerCase().hashCode();
615         hashCode += domain == null ? 0 : domain.toLowerCase().hashCode();
616         hashCode += path == null ? 0 : path.hashCode();
617         return hashCode;
618     }
619 
620     private boolean isValidName(String n) {
621         // name cannot be empty or begin with '$' or equals the reserved
622         // attributes (case-insensitive)
623         boolean isValid = !(n.length() == 0 || n.startsWith("$") || attributeSet.containsKey(n.toLowerCase()));
624         if (isValid) {
625             for (int i = 0; i < n.length(); i++) {
626                 char nameChar = n.charAt(i);
627                 // name must be ASCII characters and cannot contain ';', ',' and
628                 // whitespace
629                 if (nameChar < 0 || nameChar >= 127 || nameChar == ';' || nameChar == ','
630                         || (Character.isWhitespace(nameChar) && nameChar != ' ')) {
631                     isValid = false;
632                     break;
633                 }
634             }
635         }
636 
637         return isValid;
638     }
639 
640     /**
641      * Set the value of comment attribute(specified in RFC 2965) of this cookie.
642      * 
643      * @param purpose
644      *            the comment value to be set
645      */
646     public void setComment(String purpose) {
647         comment = purpose;
648     }
649 
650     /**
651      * Set the value of commentURL attribute(specified in RFC 2965) of this
652      * cookie.
653      * 
654      * @param purpose
655      *            the value of commentURL attribute to be set
656      */
657     public void setCommentURL(String purpose) {
658         commentURL = purpose;
659     }
660 
661     /**
662      * Set the value of discard attribute(specified in RFC 2965) of this cookie.
663      * 
664      * @param discard
665      *            the value for discard attribute
666      */
667     public void setDiscard(boolean discard) {
668         this.discard = discard;
669     }
670 
671     /**
672      * Set the domain value for this cookie. Browsers send the cookie to the
673      * domain specified by this value. The form of the domain is specified in
674      * RFC 2965.
675      * 
676      * @param pattern
677      *            the domain pattern
678      */
679     public void setDomain(String pattern) {
680         domain = pattern == null ? null : pattern.toLowerCase();
681     }
682 
683     /**
684      * Sets the Max-Age value as specified in RFC 2965 of this cookie to expire.
685      * 
686      * @param expiry
687      *            the value used to set the Max-Age value of this cookie
688      */
689     public void setMaxAge(long expiry) {
690         maxAge = expiry;
691     }
692 
693     /**
694      * Set the path to which this cookie is returned. This cookie is visible to
695      * all the pages under the path and all subpaths.
696      * 
697      * @param path
698      *            the path to which this cookie is returned
699      */
700     public void setPath(String path) {
701         this.path = path;
702     }
703 
704     /**
705      * Set the value of port attribute(specified in RFC 2965) of this cookie.
706      * 
707      * @param ports
708      *            the value for port attribute
709      */
710     public void setPortlist(String ports) {
711         portList = ports;
712     }
713 
714     /*
715      * Handle 2 special cases: 1. value is wrapped by a quotation 2. value
716      * contains comma
717      */
718 
719     /**
720      * Tells the browser whether the cookies should be sent to server through
721      * secure protocols.
722      * 
723      * @param flag
724      *            tells browser to send cookie to server only through secure
725      *            protocol if flag is true
726      */
727     public void setSecure(boolean flag) {
728         secure = flag;
729     }
730 
731     /**
732      * Sets the value for this cookie after it has been instantiated. String
733      * newValue can be in BASE64 form. If the version of the cookie is 0,
734      * special value as: white space, brackets, parentheses, equals signs,
735      * commas, double quotes, slashes, question marks, at signs, colons, and
736      * semicolons are not recommended. Empty values may lead to different
737      * behavior on different browsers.
738      * 
739      * @param newValue
740      *            the value for this cookie
741      */
742     public void setValue(String newValue) {
743         // FIXME: According to spec, version 0 cookie value does not allow many
744         // symbols. But RI does not implement it. Follow RI temporarily.
745         value = newValue;
746     }
747 
748     /**
749      * Sets the version of the cookie. 0 indicates the original Netscape cookie
750      * specification, while 1 indicates RFC 2965/2109 specification.
751      * 
752      * @param v
753      *            0 or 1 as stated above
754      * @throws IllegalArgumentException
755      *             if v is neither 0 nor 1
756      */
757     public void setVersion(int v) {
758         if (v != 0 && v != 1) {
759             throw new IllegalArgumentException("Unknown version!");
760         }
761         version = v;
762     }
763 
764     /**
765      * Returns a string to represent the cookie. The format of string follows
766      * the cookie specification. The leading token "Cookie" is not included
767      * 
768      * @return the string format of the cookie object
769      */
770     @Override
771     public String toString() {
772         StringBuilder cookieStr = new StringBuilder();
773         cookieStr.append(name);
774         cookieStr.append("=");
775         if (version == 0) {
776             cookieStr.append(value);
777         } else if (version == 1) {
778             cookieStr.append(QUOTE_STR);
779             cookieStr.append(value);
780             cookieStr.append(QUOTE_STR);
781 
782             attrToString(cookieStr, "Path", path);
783             attrToString(cookieStr, "Domain", domain);
784             attrToString(cookieStr, "Port", portList);
785         }
786 
787         return cookieStr.toString();
788     }
789 }