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 }