View Javadoc

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