815 lines
27 KiB
Java
815 lines
27 KiB
Java
/*
|
|
* 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.
|
|
*/
|
|
package org.apache.tomcat.util.http;
|
|
|
|
import java.nio.charset.Charset;
|
|
import java.nio.charset.StandardCharsets;
|
|
import java.text.FieldPosition;
|
|
import java.util.BitSet;
|
|
import java.util.Date;
|
|
|
|
import javax.servlet.http.Cookie;
|
|
|
|
import org.apache.juli.logging.Log;
|
|
import org.apache.juli.logging.LogFactory;
|
|
import org.apache.tomcat.util.buf.ByteChunk;
|
|
import org.apache.tomcat.util.buf.MessageBytes;
|
|
import org.apache.tomcat.util.log.UserDataHelper;
|
|
import org.apache.tomcat.util.res.StringManager;
|
|
|
|
/**
|
|
* The legacy (up to early Tomcat 8 releases) cookie parser based on RFC6265,
|
|
* RFC2109 and RFC2616.
|
|
*
|
|
* This class is not thread-safe.
|
|
*
|
|
* @author Costin Manolache
|
|
* @author kevin seguin
|
|
*/
|
|
public final class LegacyCookieProcessor extends CookieProcessorBase {
|
|
|
|
private static final Log log = LogFactory.getLog(LegacyCookieProcessor.class);
|
|
|
|
private static final UserDataHelper userDataLog = new UserDataHelper(log);
|
|
|
|
private static final StringManager sm =
|
|
StringManager.getManager("org.apache.tomcat.util.http");
|
|
|
|
private static final char[] V0_SEPARATORS = {',', ';', ' ', '\t'};
|
|
private static final BitSet V0_SEPARATOR_FLAGS = new BitSet(128);
|
|
|
|
// Excludes '/' since configuration controls whether or not to treat '/' as
|
|
// a separator
|
|
private static final char[] HTTP_SEPARATORS = new char[] {
|
|
'\t', ' ', '\"', '(', ')', ',', ':', ';', '<', '=', '>', '?', '@',
|
|
'[', '\\', ']', '{', '}' };
|
|
|
|
static {
|
|
for (char c : V0_SEPARATORS) {
|
|
V0_SEPARATOR_FLAGS.set(c);
|
|
}
|
|
}
|
|
|
|
private final boolean STRICT_SERVLET_COMPLIANCE =
|
|
Boolean.getBoolean("org.apache.catalina.STRICT_SERVLET_COMPLIANCE");
|
|
|
|
private boolean allowEqualsInValue = false;
|
|
|
|
private boolean allowNameOnly = false;
|
|
|
|
private boolean allowHttpSepsInV0 = false;
|
|
|
|
private boolean alwaysAddExpires = !STRICT_SERVLET_COMPLIANCE;
|
|
|
|
private final BitSet httpSeparatorFlags = new BitSet(128);
|
|
|
|
private final BitSet allowedWithoutQuotes = new BitSet(128);
|
|
|
|
|
|
public LegacyCookieProcessor() {
|
|
// BitSet elements will default to false
|
|
for (char c : HTTP_SEPARATORS) {
|
|
httpSeparatorFlags.set(c);
|
|
}
|
|
boolean b = STRICT_SERVLET_COMPLIANCE;
|
|
if (b) {
|
|
httpSeparatorFlags.set('/');
|
|
}
|
|
|
|
String separators;
|
|
if (getAllowHttpSepsInV0()) {
|
|
// comma, semi-colon and space as defined by netscape
|
|
separators = ",; ";
|
|
} else {
|
|
// separators as defined by RFC2616
|
|
separators = "()<>@,;:\\\"/[]?={} \t";
|
|
}
|
|
|
|
// all CHARs except CTLs or separators are allowed without quoting
|
|
allowedWithoutQuotes.set(0x20, 0x7f);
|
|
for (char ch : separators.toCharArray()) {
|
|
allowedWithoutQuotes.clear(ch);
|
|
}
|
|
|
|
/**
|
|
* Some browsers (e.g. IE6 and IE7) do not handle quoted Path values even
|
|
* when Version is set to 1. To allow for this, we support a property
|
|
* FWD_SLASH_IS_SEPARATOR which, when false, means a '/' character will not
|
|
* be treated as a separator, potentially avoiding quoting and the ensuing
|
|
* side effect of having the cookie upgraded to version 1.
|
|
*
|
|
* For now, we apply this rule globally rather than just to the Path attribute.
|
|
*/
|
|
if (!getAllowHttpSepsInV0() && !getForwardSlashIsSeparator()) {
|
|
allowedWithoutQuotes.set('/');
|
|
}
|
|
}
|
|
|
|
|
|
public boolean getAllowEqualsInValue() {
|
|
return allowEqualsInValue;
|
|
}
|
|
|
|
|
|
public void setAllowEqualsInValue(boolean allowEqualsInValue) {
|
|
this.allowEqualsInValue = allowEqualsInValue;
|
|
}
|
|
|
|
|
|
public boolean getAllowNameOnly() {
|
|
return allowNameOnly;
|
|
}
|
|
|
|
|
|
public void setAllowNameOnly(boolean allowNameOnly) {
|
|
this.allowNameOnly = allowNameOnly;
|
|
}
|
|
|
|
|
|
public boolean getAllowHttpSepsInV0() {
|
|
return allowHttpSepsInV0;
|
|
}
|
|
|
|
|
|
public void setAllowHttpSepsInV0(boolean allowHttpSepsInV0) {
|
|
this.allowHttpSepsInV0 = allowHttpSepsInV0;
|
|
// HTTP separators less comma, semicolon and space since the Netscape
|
|
// spec defines those as separators too.
|
|
// '/' is also treated as a special case
|
|
char[] seps = "()<>@:\\\"[]?={}\t".toCharArray();
|
|
for (char sep : seps) {
|
|
if (allowHttpSepsInV0) {
|
|
allowedWithoutQuotes.set(sep);
|
|
} else {
|
|
allowedWithoutQuotes.clear(sep);
|
|
}
|
|
}
|
|
if (getForwardSlashIsSeparator() && !allowHttpSepsInV0) {
|
|
allowedWithoutQuotes.clear('/');
|
|
} else {
|
|
allowedWithoutQuotes.set('/');
|
|
}
|
|
}
|
|
|
|
|
|
public boolean getForwardSlashIsSeparator() {
|
|
return httpSeparatorFlags.get('/');
|
|
}
|
|
|
|
|
|
public void setForwardSlashIsSeparator(boolean forwardSlashIsSeparator) {
|
|
if (forwardSlashIsSeparator) {
|
|
httpSeparatorFlags.set('/');
|
|
} else {
|
|
httpSeparatorFlags.clear('/');
|
|
}
|
|
if (forwardSlashIsSeparator && !getAllowHttpSepsInV0()) {
|
|
allowedWithoutQuotes.clear('/');
|
|
} else {
|
|
allowedWithoutQuotes.set('/');
|
|
}
|
|
}
|
|
|
|
|
|
public boolean getAlwaysAddExpires() {
|
|
return alwaysAddExpires;
|
|
}
|
|
|
|
|
|
public void setAlwaysAddExpires(boolean alwaysAddExpires) {
|
|
this.alwaysAddExpires = alwaysAddExpires;
|
|
}
|
|
|
|
|
|
@Override
|
|
public Charset getCharset() {
|
|
return StandardCharsets.ISO_8859_1;
|
|
}
|
|
|
|
|
|
@Override
|
|
public void parseCookieHeader(MimeHeaders headers, ServerCookies serverCookies) {
|
|
|
|
if (headers == null) {
|
|
// nothing to process
|
|
return;
|
|
}
|
|
// process each "cookie" header
|
|
int pos = headers.findHeader("Cookie", 0);
|
|
while (pos >= 0) {
|
|
MessageBytes cookieValue = headers.getValue(pos);
|
|
|
|
if (cookieValue != null && !cookieValue.isNull() ) {
|
|
if (cookieValue.getType() != MessageBytes.T_BYTES ) {
|
|
Exception e = new Exception();
|
|
// TODO: Review this in light of HTTP/2
|
|
log.debug("Cookies: Parsing cookie as String. Expected bytes.", e);
|
|
cookieValue.toBytes();
|
|
}
|
|
if (log.isDebugEnabled()) {
|
|
log.debug("Cookies: Parsing b[]: " + cookieValue.toString());
|
|
}
|
|
ByteChunk bc = cookieValue.getByteChunk();
|
|
processCookieHeader(bc.getBytes(), bc.getOffset(), bc.getLength(), serverCookies);
|
|
}
|
|
|
|
// search from the next position
|
|
pos = headers.findHeader("Cookie", ++pos);
|
|
}
|
|
}
|
|
|
|
|
|
@Override
|
|
public String generateHeader(Cookie cookie) {
|
|
/*
|
|
* The spec allows some latitude on when to send the version attribute
|
|
* with a Set-Cookie header. To be nice to clients, we'll make sure the
|
|
* version attribute is first. That means checking the various things
|
|
* that can cause us to switch to a v1 cookie first.
|
|
*
|
|
* Note that by checking for tokens we will also throw an exception if a
|
|
* control character is encountered.
|
|
*/
|
|
int version = cookie.getVersion();
|
|
String value = cookie.getValue();
|
|
String path = cookie.getPath();
|
|
String domain = cookie.getDomain();
|
|
String comment = cookie.getComment();
|
|
|
|
if (version == 0) {
|
|
// Check for the things that require a v1 cookie
|
|
if (needsQuotes(value, 0) || comment != null || needsQuotes(path, 0) || needsQuotes(domain, 0)) {
|
|
version = 1;
|
|
}
|
|
}
|
|
|
|
// Now build the cookie header
|
|
StringBuffer buf = new StringBuffer(); // can't use StringBuilder due to DateFormat
|
|
|
|
// Just use the name supplied in the Cookie
|
|
buf.append(cookie.getName());
|
|
buf.append("=");
|
|
|
|
// Value
|
|
maybeQuote(buf, value, version);
|
|
|
|
// Add version 1 specific information
|
|
if (version == 1) {
|
|
// Version=1 ... required
|
|
buf.append ("; Version=1");
|
|
|
|
// Comment=comment
|
|
if (comment != null) {
|
|
buf.append ("; Comment=");
|
|
maybeQuote(buf, comment, version);
|
|
}
|
|
}
|
|
|
|
// Add domain information, if present
|
|
if (domain != null) {
|
|
buf.append("; Domain=");
|
|
maybeQuote(buf, domain, version);
|
|
}
|
|
|
|
// Max-Age=secs ... or use old "Expires" format
|
|
int maxAge = cookie.getMaxAge();
|
|
if (maxAge >= 0) {
|
|
if (version > 0) {
|
|
buf.append ("; Max-Age=");
|
|
buf.append (maxAge);
|
|
}
|
|
// IE6, IE7 and possibly other browsers don't understand Max-Age.
|
|
// They do understand Expires, even with V1 cookies!
|
|
if (version == 0 || getAlwaysAddExpires()) {
|
|
// Wdy, DD-Mon-YY HH:MM:SS GMT ( Expires Netscape format )
|
|
buf.append ("; Expires=");
|
|
// To expire immediately we need to set the time in past
|
|
if (maxAge == 0) {
|
|
buf.append( ANCIENT_DATE );
|
|
} else {
|
|
COOKIE_DATE_FORMAT.get().format(
|
|
new Date(System.currentTimeMillis() + maxAge * 1000L),
|
|
buf,
|
|
new FieldPosition(0));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Path=path
|
|
if (path!=null) {
|
|
buf.append ("; Path=");
|
|
maybeQuote(buf, path, version);
|
|
}
|
|
|
|
// Secure
|
|
if (cookie.getSecure()) {
|
|
buf.append ("; Secure");
|
|
}
|
|
|
|
// HttpOnly
|
|
if (cookie.isHttpOnly()) {
|
|
buf.append("; HttpOnly");
|
|
}
|
|
|
|
SameSiteCookies sameSiteCookiesValue = getSameSiteCookies();
|
|
|
|
if (!sameSiteCookiesValue.equals(SameSiteCookies.UNSET)) {
|
|
buf.append("; SameSite=");
|
|
buf.append(sameSiteCookiesValue.getValue());
|
|
}
|
|
|
|
return buf.toString();
|
|
}
|
|
|
|
|
|
private void maybeQuote(StringBuffer buf, String value, int version) {
|
|
if (value == null || value.length() == 0) {
|
|
buf.append("\"\"");
|
|
} else if (alreadyQuoted(value)) {
|
|
buf.append('"');
|
|
escapeDoubleQuotes(buf, value,1,value.length()-1);
|
|
buf.append('"');
|
|
} else if (needsQuotes(value, version)) {
|
|
buf.append('"');
|
|
escapeDoubleQuotes(buf, value,0,value.length());
|
|
buf.append('"');
|
|
} else {
|
|
buf.append(value);
|
|
}
|
|
}
|
|
|
|
|
|
private static void escapeDoubleQuotes(StringBuffer b, String s, int beginIndex, int endIndex) {
|
|
if (s.indexOf('"') == -1 && s.indexOf('\\') == -1) {
|
|
b.append(s);
|
|
return;
|
|
}
|
|
|
|
for (int i = beginIndex; i < endIndex; i++) {
|
|
char c = s.charAt(i);
|
|
if (c == '\\' ) {
|
|
b.append('\\').append('\\');
|
|
} else if (c == '"') {
|
|
b.append('\\').append('"');
|
|
} else {
|
|
b.append(c);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
private boolean needsQuotes(String value, int version) {
|
|
if (value == null) {
|
|
return false;
|
|
}
|
|
|
|
int i = 0;
|
|
int len = value.length();
|
|
|
|
if (alreadyQuoted(value)) {
|
|
i++;
|
|
len--;
|
|
}
|
|
|
|
for (; i < len; i++) {
|
|
char c = value.charAt(i);
|
|
if ((c < 0x20 && c != '\t') || c >= 0x7f) {
|
|
throw new IllegalArgumentException(
|
|
"Control character in cookie value or attribute.");
|
|
}
|
|
if (version == 0 && !allowedWithoutQuotes.get(c) ||
|
|
version == 1 && isHttpSeparator(c)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
private static boolean alreadyQuoted (String value) {
|
|
return value.length() >= 2 &&
|
|
value.charAt(0) == '\"' &&
|
|
value.charAt(value.length() - 1) == '\"';
|
|
}
|
|
|
|
|
|
/**
|
|
* Parses a cookie header after the initial "Cookie:"
|
|
* [WS][$]token[WS]=[WS](token|QV)[;|,]
|
|
* RFC 2965 / RFC 2109
|
|
* JVK
|
|
*/
|
|
private final void processCookieHeader(byte bytes[], int off, int len,
|
|
ServerCookies serverCookies) {
|
|
|
|
if (len <= 0 || bytes == null) {
|
|
return;
|
|
}
|
|
int end = off + len;
|
|
int pos = off;
|
|
int nameStart = 0;
|
|
int nameEnd = 0;
|
|
int valueStart = 0;
|
|
int valueEnd = 0;
|
|
int version = 0;
|
|
ServerCookie sc = null;
|
|
boolean isSpecial;
|
|
boolean isQuoted;
|
|
|
|
while (pos < end) {
|
|
isSpecial = false;
|
|
isQuoted = false;
|
|
|
|
// Skip whitespace and non-token characters (separators)
|
|
while (pos < end &&
|
|
(isHttpSeparator((char) bytes[pos]) &&
|
|
!getAllowHttpSepsInV0() ||
|
|
isV0Separator((char) bytes[pos]) ||
|
|
isWhiteSpace(bytes[pos])))
|
|
{pos++; }
|
|
|
|
if (pos >= end) {
|
|
return;
|
|
}
|
|
|
|
// Detect Special cookies
|
|
if (bytes[pos] == '$') {
|
|
isSpecial = true;
|
|
pos++;
|
|
}
|
|
|
|
// Get the cookie/attribute name. This must be a token
|
|
valueEnd = valueStart = nameStart = pos;
|
|
pos = nameEnd = getTokenEndPosition(bytes,pos,end,version,true);
|
|
|
|
// Skip whitespace
|
|
while (pos < end && isWhiteSpace(bytes[pos])) {pos++; }
|
|
|
|
|
|
// Check for an '=' -- This could also be a name-only
|
|
// cookie at the end of the cookie header, so if we
|
|
// are past the end of the header, but we have a name
|
|
// skip to the name-only part.
|
|
if (pos < (end - 1) && bytes[pos] == '=') {
|
|
|
|
// Skip whitespace
|
|
do {
|
|
pos++;
|
|
} while (pos < end && isWhiteSpace(bytes[pos]));
|
|
|
|
if (pos >= end) {
|
|
return;
|
|
}
|
|
|
|
// Determine what type of value this is, quoted value,
|
|
// token, name-only with an '=', or other (bad)
|
|
switch (bytes[pos]) {
|
|
case '"': // Quoted Value
|
|
isQuoted = true;
|
|
valueStart = pos + 1; // strip "
|
|
// getQuotedValue returns the position before
|
|
// at the last quote. This must be dealt with
|
|
// when the bytes are copied into the cookie
|
|
valueEnd = getQuotedValueEndPosition(bytes, valueStart, end);
|
|
// We need pos to advance
|
|
pos = valueEnd;
|
|
// Handles cases where the quoted value is
|
|
// unterminated and at the end of the header,
|
|
// e.g. [myname="value]
|
|
if (pos >= end) {
|
|
return;
|
|
}
|
|
break;
|
|
case ';':
|
|
case ',':
|
|
// Name-only cookie with an '=' after the name token
|
|
// This may not be RFC compliant
|
|
valueStart = valueEnd = -1;
|
|
// The position is OK (On a delimiter)
|
|
break;
|
|
default:
|
|
if (version == 0 &&
|
|
!isV0Separator((char)bytes[pos]) &&
|
|
getAllowHttpSepsInV0() ||
|
|
!isHttpSeparator((char)bytes[pos]) ||
|
|
bytes[pos] == '=') {
|
|
// Token
|
|
valueStart = pos;
|
|
// getToken returns the position at the delimiter
|
|
// or other non-token character
|
|
valueEnd = getTokenEndPosition(bytes, valueStart, end, version, false);
|
|
// We need pos to advance
|
|
pos = valueEnd;
|
|
// Edge case. If value starts with '=' but this is not
|
|
// allowed in a value make sure we treat this as no
|
|
// value being present
|
|
if (valueStart == valueEnd) {
|
|
valueStart = -1;
|
|
valueEnd = -1;
|
|
}
|
|
} else {
|
|
// INVALID COOKIE, advance to next delimiter
|
|
// The starting character of the cookie value was
|
|
// not valid.
|
|
UserDataHelper.Mode logMode = userDataLog.getNextMode();
|
|
if (logMode != null) {
|
|
String message = sm.getString(
|
|
"cookies.invalidCookieToken");
|
|
switch (logMode) {
|
|
case INFO_THEN_DEBUG:
|
|
message += sm.getString(
|
|
"cookies.fallToDebug");
|
|
//$FALL-THROUGH$
|
|
case INFO:
|
|
log.info(message);
|
|
break;
|
|
case DEBUG:
|
|
log.debug(message);
|
|
}
|
|
}
|
|
while (pos < end && bytes[pos] != ';' &&
|
|
bytes[pos] != ',')
|
|
{pos++; }
|
|
pos++;
|
|
// Make sure no special avpairs can be attributed to
|
|
// the previous cookie by setting the current cookie
|
|
// to null
|
|
sc = null;
|
|
continue;
|
|
}
|
|
}
|
|
} else {
|
|
// Name only cookie
|
|
valueStart = valueEnd = -1;
|
|
pos = nameEnd;
|
|
|
|
}
|
|
|
|
// We should have an avpair or name-only cookie at this
|
|
// point. Perform some basic checks to make sure we are
|
|
// in a good state.
|
|
|
|
// Skip whitespace
|
|
while (pos < end && isWhiteSpace(bytes[pos])) {pos++; }
|
|
|
|
|
|
// Make sure that after the cookie we have a separator. This
|
|
// is only important if this is not the last cookie pair
|
|
while (pos < end && bytes[pos] != ';' && bytes[pos] != ',') {
|
|
pos++;
|
|
}
|
|
|
|
pos++;
|
|
|
|
// All checks passed. Add the cookie, start with the
|
|
// special avpairs first
|
|
if (isSpecial) {
|
|
isSpecial = false;
|
|
// $Version must be the first avpair in the cookie header
|
|
// (sc must be null)
|
|
if (equals( "Version", bytes, nameStart, nameEnd) &&
|
|
sc == null) {
|
|
// Set version
|
|
if( bytes[valueStart] =='1' && valueEnd == (valueStart+1)) {
|
|
version=1;
|
|
} else {
|
|
// unknown version (Versioning is not very strict)
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// We need an active cookie for Path/Port/etc.
|
|
if (sc == null) {
|
|
continue;
|
|
}
|
|
|
|
// Domain is more common, so it goes first
|
|
if (equals( "Domain", bytes, nameStart, nameEnd)) {
|
|
sc.getDomain().setBytes( bytes,
|
|
valueStart,
|
|
valueEnd-valueStart);
|
|
continue;
|
|
}
|
|
|
|
if (equals( "Path", bytes, nameStart, nameEnd)) {
|
|
sc.getPath().setBytes( bytes,
|
|
valueStart,
|
|
valueEnd-valueStart);
|
|
continue;
|
|
}
|
|
|
|
// v2 cookie attributes - skip them
|
|
if (equals( "Port", bytes, nameStart, nameEnd)) {
|
|
continue;
|
|
}
|
|
if (equals( "CommentURL", bytes, nameStart, nameEnd)) {
|
|
continue;
|
|
}
|
|
|
|
// Unknown cookie, complain
|
|
UserDataHelper.Mode logMode = userDataLog.getNextMode();
|
|
if (logMode != null) {
|
|
String message = sm.getString("cookies.invalidSpecial");
|
|
switch (logMode) {
|
|
case INFO_THEN_DEBUG:
|
|
message += sm.getString("cookies.fallToDebug");
|
|
//$FALL-THROUGH$
|
|
case INFO:
|
|
log.info(message);
|
|
break;
|
|
case DEBUG:
|
|
log.debug(message);
|
|
}
|
|
}
|
|
} else { // Normal Cookie
|
|
if (valueStart == -1 && !getAllowNameOnly()) {
|
|
// Skip name only cookies if not supported
|
|
continue;
|
|
}
|
|
|
|
sc = serverCookies.addCookie();
|
|
sc.setVersion( version );
|
|
sc.getName().setBytes( bytes, nameStart,
|
|
nameEnd-nameStart);
|
|
|
|
if (valueStart != -1) { // Normal AVPair
|
|
sc.getValue().setBytes( bytes, valueStart,
|
|
valueEnd-valueStart);
|
|
if (isQuoted) {
|
|
// We know this is a byte value so this is safe
|
|
unescapeDoubleQuotes(sc.getValue().getByteChunk());
|
|
}
|
|
} else {
|
|
// Name Only
|
|
sc.getValue().setString("");
|
|
}
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Given the starting position of a token, this gets the end of the
|
|
* token, with no separator characters in between.
|
|
* JVK
|
|
*/
|
|
private final int getTokenEndPosition(byte bytes[], int off, int end,
|
|
int version, boolean isName){
|
|
int pos = off;
|
|
while (pos < end &&
|
|
(!isHttpSeparator((char)bytes[pos]) ||
|
|
version == 0 && getAllowHttpSepsInV0() && bytes[pos] != '=' &&
|
|
!isV0Separator((char)bytes[pos]) ||
|
|
!isName && bytes[pos] == '=' && getAllowEqualsInValue())) {
|
|
pos++;
|
|
}
|
|
|
|
if (pos > end) {
|
|
return end;
|
|
}
|
|
return pos;
|
|
}
|
|
|
|
|
|
private boolean isHttpSeparator(final char c) {
|
|
if (c < 0x20 || c >= 0x7f) {
|
|
if (c != 0x09) {
|
|
throw new IllegalArgumentException(
|
|
"Control character in cookie value or attribute.");
|
|
}
|
|
}
|
|
|
|
return httpSeparatorFlags.get(c);
|
|
}
|
|
|
|
|
|
/**
|
|
* Returns true if the byte is a separator as defined by V0 of the cookie
|
|
* spec.
|
|
*/
|
|
private static boolean isV0Separator(final char c) {
|
|
if (c < 0x20 || c >= 0x7f) {
|
|
if (c != 0x09) {
|
|
throw new IllegalArgumentException(
|
|
"Control character in cookie value or attribute.");
|
|
}
|
|
}
|
|
|
|
return V0_SEPARATOR_FLAGS.get(c);
|
|
}
|
|
|
|
|
|
/**
|
|
* Given a starting position after an initial quote character, this gets
|
|
* the position of the end quote. This escapes anything after a '\' char
|
|
* JVK RFC 2616
|
|
*/
|
|
private static final int getQuotedValueEndPosition(byte bytes[], int off, int end){
|
|
int pos = off;
|
|
while (pos < end) {
|
|
if (bytes[pos] == '"') {
|
|
return pos;
|
|
} else if (bytes[pos] == '\\' && pos < (end - 1)) {
|
|
pos+=2;
|
|
} else {
|
|
pos++;
|
|
}
|
|
}
|
|
// Error, we have reached the end of the header w/o a end quote
|
|
return end;
|
|
}
|
|
|
|
|
|
private static final boolean equals(String s, byte b[], int start, int end) {
|
|
int blen = end-start;
|
|
if (b == null || blen != s.length()) {
|
|
return false;
|
|
}
|
|
int boff = start;
|
|
for (int i = 0; i < blen; i++) {
|
|
if (b[boff++] != s.charAt(i)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
/**
|
|
* Returns true if the byte is a whitespace character as
|
|
* defined in RFC2619
|
|
* JVK
|
|
*/
|
|
private static final boolean isWhiteSpace(final byte c) {
|
|
// This switch statement is slightly slower
|
|
// for my vm than the if statement.
|
|
// Java(TM) 2 Runtime Environment, Standard Edition (build 1.5.0_07-164)
|
|
/*
|
|
switch (c) {
|
|
case ' ':;
|
|
case '\t':;
|
|
case '\n':;
|
|
case '\r':;
|
|
case '\f':;
|
|
return true;
|
|
default:;
|
|
return false;
|
|
}
|
|
*/
|
|
if (c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f') {
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Unescapes any double quotes in the given cookie value.
|
|
*
|
|
* @param bc The cookie value to modify
|
|
*/
|
|
private static final void unescapeDoubleQuotes(ByteChunk bc) {
|
|
|
|
if (bc == null || bc.getLength() == 0 || bc.indexOf('"', 0) == -1) {
|
|
return;
|
|
}
|
|
|
|
// Take a copy of the buffer so the original cookie header is not
|
|
// modified by this unescaping.
|
|
byte[] original = bc.getBuffer();
|
|
int len = bc.getLength();
|
|
|
|
byte[] copy = new byte[len];
|
|
System.arraycopy(original, bc.getStart(), copy, 0, len);
|
|
|
|
int src = 0;
|
|
int dest = 0;
|
|
|
|
while (src < len) {
|
|
if (copy[src] == '\\' && src < len && copy[src+1] == '"') {
|
|
src++;
|
|
}
|
|
copy[dest] = copy[src];
|
|
dest ++;
|
|
src ++;
|
|
}
|
|
bc.setBytes(copy, 0, dest);
|
|
}
|
|
}
|