blob: 4db1ab9de62da929334650f4b50eeac5469eafdc [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/*
* This class has been taken from Apache Harmony (http://harmony.apache.org/)
* and has been modified to work with OpenCMIS.
*/
package org.apache.chemistry.opencmis.client.bindings.spi.cookies;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.chemistry.opencmis.commons.impl.DateTimeHelper;
/**
* This class represents a http cookie, which indicates the status information
* between the client agent side and the server side. According to RFC, there
* are 4 http cookie specifications. This class is compatible with the original
* Netscape specification, RFC 2109, RFC 2965 and party compatible with RFC
* 6265. HttpCookie class can accept all syntax forms.
*/
public final class CmisHttpCookie implements Cloneable, Serializable {
private static final long serialVersionUID = 1L;
private static final String DOT_STR = ".";
private static final String LOCAL_STR = ".local";
private static final String QUOTE_STR = "\"";
private static final String COMMA_STR = ",";
private static final Pattern HEAD_PATTERN = Pattern.compile("Set-Cookie2?:", Pattern.CASE_INSENSITIVE);
private static final Pattern NAME_PATTERN = Pattern.compile(
"([^$=,\u0085\u2028\u2029][^,\n\t\r\r\n\u0085\u2028\u2029]*?)=([^;]*)(;)?", Pattern.DOTALL
| Pattern.CASE_INSENSITIVE);
private static final Pattern ATTR_PATTERN0 = Pattern.compile("([^;=]*)(?:=([^;]*))?");
private static final Pattern ATTR_PATTERN1 = Pattern.compile("(,?[^;=]*)(?:=([^;,]*))?((?=.))?");
private abstract static class Setter {
private boolean set;
Setter() {
set = false;
}
boolean isSet() {
return set;
}
void set(boolean isSet) {
set = isSet;
}
abstract void setValue(String value, CmisHttpCookie cookie);
void validate(String value, CmisHttpCookie cookie) {
if (cookie.getVersion() == 1 && value != null && value.contains(COMMA_STR)) {
throw new IllegalArgumentException();
}
}
}
private Map<String, Setter> attributeSet = new HashMap<String, Setter>();
/**
* A utility method used to check whether the host name is in a domain or
* not.
*
* @param domain
* the domain to be checked against
* @param host
* the host to be checked
* @return true if the host is in the domain, false otherwise
*/
public static boolean domainMatches(String domain, String host) {
if (domain == null || host == null) {
return false;
}
String newDomain = domain.toLowerCase();
String newHost = host.toLowerCase();
return newDomain.equals(newHost)
|| (isValidDomain(newDomain) && effDomainMatches(newDomain, newHost) && isValidHost(newDomain, newHost));
}
private static boolean effDomainMatches(String domain, String host) {
// calculate effective host name
String effHost = host.indexOf(DOT_STR) != -1 ? host : (host + LOCAL_STR);
// Rule 2: domain and host are string-compare equal, or A = NB, B = .B'
// and N is a non-empty name string
boolean inDomain = domain.equals(effHost);
inDomain = inDomain
|| (effHost.endsWith(domain) && effHost.length() > domain.length() && domain.startsWith(DOT_STR));
return inDomain;
}
private static boolean isCommaDelim(CmisHttpCookie cookie) {
String value = cookie.getValue();
if (value.startsWith(QUOTE_STR) && value.endsWith(QUOTE_STR)) {
cookie.setValue(value.substring(1, value.length() - 1));
return false;
}
if (cookie.getVersion() == 1 && value.contains(COMMA_STR)) {
cookie.setValue(value.substring(0, value.indexOf(COMMA_STR)));
return true;
}
return false;
}
private static boolean isValidDomain(String domain) {
// Rule 1: The value for Domain contains embedded dots, or is .local
if (domain.length() <= 2) {
return false;
}
return domain.substring(1, domain.length() - 1).indexOf(DOT_STR) != -1 || domain.equals(LOCAL_STR);
}
private static boolean isValidHost(String domain, String host) {
// Rule 3: host does not end with domain, or the remainder does not
// contain "."
boolean matches = !host.endsWith(domain);
if (!matches) {
String hostSub = host.substring(0, host.length() - domain.length());
matches = hostSub.indexOf(DOT_STR) == -1;
}
return matches;
}
/**
* Constructs a cookie from a string. The string should comply with
* set-cookie or set-cookie2 header format as specified in RFC 2965. Since
* set-cookies2 syntax allows more than one cookie definitions in one
* header, the returned object is a list.
*
* @param header
* a set-cookie or set-cookie2 header.
* @return a list of constructed cookies
* @throws IllegalArgumentException
* if the string does not comply with cookie specification, or
* the cookie name contains illegal characters, or reserved
* tokens of cookie specification appears
* @throws NullPointerException
* if header is null
*/
public static List<CmisHttpCookie> parse(String header) {
Matcher matcher = HEAD_PATTERN.matcher(header);
// Parse cookie name & value
List<CmisHttpCookie> list = null;
CmisHttpCookie cookie = null;
String headerString = header;
int version = 0;
// process set-cookie | set-cookie2 head
if (matcher.find()) {
String cookieHead = matcher.group();
if ("set-cookie2:".equalsIgnoreCase(cookieHead)) {
version = 1;
}
headerString = header.substring(cookieHead.length());
}
// parse cookie name/value pair
matcher = NAME_PATTERN.matcher(headerString);
if (matcher.lookingAt()) {
list = new ArrayList<CmisHttpCookie>();
cookie = new CmisHttpCookie(matcher.group(1), matcher.group(2));
cookie.setVersion(version);
/*
* Comma is a delimiter in cookie spec 1.1. If find comma in version
* 1 cookie header, part of matched string need to be spitted out.
*/
String nameGroup = matcher.group();
if (isCommaDelim(cookie)) {
headerString = headerString.substring(nameGroup.indexOf(COMMA_STR));
} else {
headerString = headerString.substring(nameGroup.length());
}
list.add(cookie);
} else {
throw new IllegalArgumentException();
}
// parse cookie headerString
while (!(headerString.length() == 0)) {
matcher = cookie.getVersion() == 1 ? ATTR_PATTERN1.matcher(headerString) : ATTR_PATTERN0
.matcher(headerString);
if (matcher.lookingAt()) {
String attrName = matcher.group(1).trim();
// handle special situation like: <..>;;<..>
if (attrName.length() == 0) {
headerString = headerString.substring(1);
continue;
}
// If port is the attribute, then comma will not be used as a
// delimiter
if (attrName.equalsIgnoreCase("port") || attrName.equalsIgnoreCase("expires")) {
int start = matcher.regionStart();
matcher = ATTR_PATTERN0.matcher(headerString);
matcher.region(start, headerString.length());
matcher.lookingAt();
} else if (cookie.getVersion() == 1 && attrName.startsWith(COMMA_STR)) {
// If the last encountered token is comma, and the parsed
// attribute is not port, then this attribute/value pair
// ends.
headerString = headerString.substring(1);
matcher = NAME_PATTERN.matcher(headerString);
if (matcher.lookingAt()) {
cookie = new CmisHttpCookie(matcher.group(1), matcher.group(2));
list.add(cookie);
headerString = headerString.substring(matcher.group().length());
continue;
}
}
Setter setter = cookie.attributeSet.get(attrName.toLowerCase());
if (setter != null && !setter.isSet()) {
String attrValue = matcher.group(2);
setter.validate(attrValue, cookie);
setter.setValue(matcher.group(2), cookie);
}
headerString = headerString.substring(matcher.end());
}
}
return list;
}
private String comment;
private String commentURL;
private boolean discard;
private String domain;
private long maxAge = -1L;
private String name;
private String path;
private String portList;
private boolean secure;
private String value;
private int version = 1;
{
attributeSet.put("comment", new Setter() {
@Override
void setValue(String value, CmisHttpCookie cookie) {
cookie.setComment(value);
if (cookie.getComment() != null) {
set(true);
}
}
});
attributeSet.put("commenturl", new Setter() {
@Override
void setValue(String value, CmisHttpCookie cookie) {
cookie.setCommentURL(value);
if (cookie.getCommentURL() != null) {
set(true);
}
}
});
attributeSet.put("discard", new Setter() {
@Override
void setValue(String value, CmisHttpCookie cookie) {
cookie.setDiscard(true);
set(true);
}
});
attributeSet.put("domain", new Setter() {
@Override
void setValue(String value, CmisHttpCookie cookie) {
cookie.setDomain(value);
if (cookie.getDomain() != null) {
set(true);
}
}
});
attributeSet.put("max-age", new Setter() {
@Override
void setValue(String value, CmisHttpCookie cookie) {
try {
cookie.setMaxAge(Long.parseLong(value));
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Invalid max-age!", e);
}
set(true);
if (!attributeSet.get("version").isSet()) {
cookie.setVersion(1);
}
}
});
attributeSet.put("path", new Setter() {
@Override
void setValue(String value, CmisHttpCookie cookie) {
cookie.setPath(value);
if (cookie.getPath() != null) {
set(true);
}
}
});
attributeSet.put("port", new Setter() {
@Override
void setValue(String value, CmisHttpCookie cookie) {
cookie.setPortlist(value);
if (cookie.getPortlist() != null) {
set(true);
}
}
@Override
void validate(String v, CmisHttpCookie cookie) {
}
});
attributeSet.put("secure", new Setter() {
@Override
void setValue(String value, CmisHttpCookie cookie) {
cookie.setSecure(true);
set(true);
}
});
attributeSet.put("version", new Setter() {
@Override
void setValue(String value, CmisHttpCookie cookie) {
try {
int v = Integer.parseInt(value);
if (v > cookie.getVersion()) {
cookie.setVersion(v);
}
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Invalid version!", e);
}
if (cookie.getVersion() != 0) {
set(true);
}
}
});
attributeSet.put("expires", new Setter() {
@Override
void setValue(String value, CmisHttpCookie cookie) {
cookie.setVersion(0);
attributeSet.get("version").set(true);
if (!attributeSet.get("max-age").isSet()) {
attributeSet.get("max-age").set(true);
if (!"en".equalsIgnoreCase(Locale.getDefault().getLanguage())) {
cookie.setMaxAge(0);
return;
}
Date date = DateTimeHelper.parseHttpDateTime(value);
if (date != null) {
cookie.setMaxAge((date.getTime() - System.currentTimeMillis()) / 1000);
} else {
cookie.setMaxAge(0);
}
}
}
@Override
void validate(String v, CmisHttpCookie cookie) {
}
});
}
/**
* Initializes a cookie with the specified name and value.
*
* The name attribute can just contain ASCII characters, which is immutable
* after creation. Commas, white space and semicolons are not allowed. The $
* character is also not allowed to be the beginning of the name.
*
* The value attribute depends on what the server side is interested. The
* setValue method can be used to change it.
*
* RFC 2965 is the default cookie specification of this class. If one wants
* to change the version of the cookie, the setVersion method is available.
*
* @param name
* - the specific name of the cookie
* @param value
* - the specific value of the cookie
*
* @throws IllegalArgumentException
* - if the name contains not-allowed or reserved characters
*
* @throws NullPointerException
* if the value of name is null
*/
public CmisHttpCookie(String name, String value) {
String ntrim = name.trim(); // erase leading and trailing whitespaces
if (!isValidName(ntrim)) {
throw new IllegalArgumentException("Invalid name!");
}
this.name = ntrim;
this.value = value;
}
private void attrToString(StringBuilder builder, String attrName, String attrValue) {
if (attrValue != null && builder != null) {
builder.append(';');
builder.append('$');
builder.append(attrName);
builder.append("=\"");
builder.append(attrValue);
builder.append(QUOTE_STR);
}
}
/**
* Answers a copy of this object.
*
* @return a copy of this cookie
*/
@Override
public Object clone() {
try {
return super.clone();
} catch (CloneNotSupportedException e) {
return null;
}
}
/**
* Answers whether two cookies are equal. Two cookies are equal if they have
* the same domain and name in a case-insensitive mode and path in a
* case-sensitive mode.
*
* @param obj
* the object to be compared.
* @return true if two cookies equals, false otherwise
*/
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (obj instanceof CmisHttpCookie) {
CmisHttpCookie anotherCookie = (CmisHttpCookie) obj;
if (name.equalsIgnoreCase(anotherCookie.getName())) {
String anotherDomain = anotherCookie.getDomain();
boolean equals = domain == null ? anotherDomain == null : domain.equalsIgnoreCase(anotherDomain);
if (equals) {
String anotherPath = anotherCookie.getPath();
return path == null ? anotherPath == null : path.equals(anotherPath);
}
}
}
return false;
}
/**
* Answers the value of comment attribute(specified in RFC 2965) of this
* cookie.
*
* @return the value of comment attribute
*/
public String getComment() {
return comment;
}
/**
* Answers the value of commentURL attribute(specified in RFC 2965) of this
* cookie.
*
* @return the value of commentURL attribute
*/
public String getCommentURL() {
return commentURL;
}
/**
* Answers the value of discard attribute(specified in RFC 2965) of this
* cookie.
*
* @return discard value of this cookie
*/
public boolean getDiscard() {
return discard;
}
/**
* Answers the domain name for this cookie in the format specified in RFC
* 2965
*
* @return the domain value of this cookie
*/
public String getDomain() {
return domain;
}
/**
* Returns the Max-Age value as specified in RFC 2965 of this cookie.
*
* @return the Max-Age value
*/
public long getMaxAge() {
return maxAge;
}
/**
* Answers the name for this cookie.
*
* @return the name for this cookie
*/
public String getName() {
return name;
}
/**
* Answers the path part of a request URL to which this cookie is returned.
* This cookie is visible to all subpaths.
*
* @return the path used to return the cookie
*/
public String getPath() {
return path;
}
/**
* Answers the value of port attribute(specified in RFC 2965) of this
* cookie.
*
* @return port list of this cookie
*/
public String getPortlist() {
return portList;
}
/**
* Answers true if the browser only sends cookies over a secure protocol.
* False if can send cookies through any protocols.
*
* @return true if sends cookies only through secure protocol, false
* otherwise
*/
public boolean getSecure() {
return secure;
}
/**
* Answers the value of this cookie.
*
* @return the value of this cookie
*/
public String getValue() {
return value;
}
/**
* Get the version of this cookie
*
* @return 0 indicates the original Netscape cookie specification, while 1
* indicates RFC 2965/2109 specification.
*/
public int getVersion() {
return version;
}
/**
* Answers whether the cookie has expired.
*
* @return true is the cookie has expired, false otherwise
*/
public boolean hasExpired() {
// -1 indicates the cookie will persist until browser shutdown
// so the cookie is not expired.
if (maxAge == -1L) {
return false;
}
boolean expired = false;
if (maxAge <= 0L) {
expired = true;
}
return expired;
}
/**
* Answers hash code of this http cookie. The result is calculated as below:
*
* getName().toLowerCase().hashCode() + getDomain().toLowerCase().hashCode()
* + getPath().hashCode()
*
* @return the hash code of this cookie
*/
@Override
public int hashCode() {
int hashCode = name.toLowerCase().hashCode();
hashCode += domain == null ? 0 : domain.toLowerCase().hashCode();
hashCode += path == null ? 0 : path.hashCode();
return hashCode;
}
private boolean isValidName(String n) {
// name cannot be empty or begin with '$' or equals the reserved
// attributes (case-insensitive)
boolean isValid = !(n.length() == 0 || n.charAt(0) == '$' || attributeSet.containsKey(n.toLowerCase()));
if (isValid) {
for (int i = 0; i < n.length(); i++) {
char nameChar = n.charAt(i);
// name must be ASCII characters and cannot contain ';', ',' and
// whitespace
if (nameChar < 0 || nameChar >= 127 || nameChar == ';' || nameChar == ','
|| (Character.isWhitespace(nameChar) && nameChar != ' ')) {
isValid = false;
break;
}
}
}
return isValid;
}
/**
* Set the value of comment attribute(specified in RFC 2965) of this cookie.
*
* @param purpose
* the comment value to be set
*/
public void setComment(String purpose) {
comment = purpose;
}
/**
* Set the value of commentURL attribute(specified in RFC 2965) of this
* cookie.
*
* @param purpose
* the value of commentURL attribute to be set
*/
public void setCommentURL(String purpose) {
commentURL = purpose;
}
/**
* Set the value of discard attribute(specified in RFC 2965) of this cookie.
*
* @param discard
* the value for discard attribute
*/
public void setDiscard(boolean discard) {
this.discard = discard;
}
/**
* Set the domain value for this cookie. Browsers send the cookie to the
* domain specified by this value. The form of the domain is specified in
* RFC 2965.
*
* @param pattern
* the domain pattern
*/
public void setDomain(String pattern) {
domain = pattern == null ? null : pattern.toLowerCase();
}
/**
* Sets the Max-Age value as specified in RFC 2965 of this cookie to expire.
*
* @param expiry
* the value used to set the Max-Age value of this cookie
*/
public void setMaxAge(long expiry) {
maxAge = expiry;
}
/**
* Set the path to which this cookie is returned. This cookie is visible to
* all the pages under the path and all subpaths.
*
* @param path
* the path to which this cookie is returned
*/
public void setPath(String path) {
this.path = path;
}
/**
* Set the value of port attribute(specified in RFC 2965) of this cookie.
*
* @param ports
* the value for port attribute
*/
public void setPortlist(String ports) {
portList = ports;
}
/*
* Handle 2 special cases: 1. value is wrapped by a quotation 2. value
* contains comma
*/
/**
* Tells the browser whether the cookies should be sent to server through
* secure protocols.
*
* @param flag
* tells browser to send cookie to server only through secure
* protocol if flag is true
*/
public void setSecure(boolean flag) {
secure = flag;
}
/**
* Sets the value for this cookie after it has been instantiated. String
* newValue can be in BASE64 form. If the version of the cookie is 0,
* special value as: white space, brackets, parentheses, equals signs,
* commas, double quotes, slashes, question marks, at signs, colons, and
* semicolons are not recommended. Empty values may lead to different
* behavior on different browsers.
*
* @param newValue
* the value for this cookie
*/
public void setValue(String newValue) {
// FIXME: According to spec, version 0 cookie value does not allow many
// symbols. But RI does not implement it. Follow RI temporarily.
value = newValue;
}
/**
* Sets the version of the cookie. 0 indicates the original Netscape cookie
* specification, while 1 indicates RFC 2965/2109 specification.
*
* @param v
* 0 or 1 as stated above
* @throws IllegalArgumentException
* if v is neither 0 nor 1
*/
public void setVersion(int v) {
if (v != 0 && v != 1) {
throw new IllegalArgumentException("Unknown version!");
}
version = v;
}
/**
* Returns a string to represent the cookie. The format of string follows
* the cookie specification. The leading token "Cookie" is not included
*
* @return the string format of the cookie object
*/
@Override
public String toString() {
StringBuilder cookieStr = new StringBuilder();
cookieStr.append(name);
cookieStr.append('=');
if (version == 0) {
cookieStr.append(value);
} else if (version == 1) {
cookieStr.append(QUOTE_STR);
cookieStr.append(value);
cookieStr.append(QUOTE_STR);
attrToString(cookieStr, "Path", path);
attrToString(cookieStr, "Domain", domain);
attrToString(cookieStr, "Port", portList);
}
return cookieStr.toString();
}
}