/*
 * 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.parser;

import java.nio.charset.StandardCharsets;

import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import org.apache.tomcat.util.http.ServerCookie;
import org.apache.tomcat.util.http.ServerCookies;
import org.apache.tomcat.util.log.UserDataHelper;
import org.apache.tomcat.util.res.StringManager;


Cookie header parser based on RFC6265 and RFC2109.

The parsing of cookies using RFC6265 is more relaxed that the specification in the following ways:

  • Values 0x80 to 0xFF are permitted in cookie-octet to support the use of UTF-8 in cookie values as used by HTML 5.
  • For cookies without a value, the '=' is not required after the name as some browsers do not sent it.

The parsing of cookies using RFC2109 is more relaxed that the specification in the following ways:

  • Values for the path attribute that contain a / character do not have to be quoted even though / is not permitted in a token.

Implementation note:
This class has been carefully tuned to ensure that it has equal or better performance than the original Netscape/RFC2109 cookie parser. Before committing and changes, ensure that the TesterCookiePerformance unit test continues to give results within 1% for the old and new parsers.

/** * <p>Cookie header parser based on RFC6265 and RFC2109.</p> * <p>The parsing of cookies using RFC6265 is more relaxed that the * specification in the following ways:</p> * <ul> * <li>Values 0x80 to 0xFF are permitted in cookie-octet to support the use of * UTF-8 in cookie values as used by HTML 5.</li> * <li>For cookies without a value, the '=' is not required after the name as * some browsers do not sent it.</li> * </ul> * <p>The parsing of cookies using RFC2109 is more relaxed that the * specification in the following ways:</p> * <ul> * <li>Values for the path attribute that contain a / character do not have to * be quoted even though / is not permitted in a token.</li> * </ul> * * <p>Implementation note:<br> * This class has been carefully tuned to ensure that it has equal or better * performance than the original Netscape/RFC2109 cookie parser. Before * committing and changes, ensure that the TesterCookiePerformance unit test * continues to give results within 1% for the old and new parsers.</p> */
public class Cookie { private static final Log log = LogFactory.getLog(Cookie.class); private static final UserDataHelper invalidCookieVersionLog = new UserDataHelper(log); private static final UserDataHelper invalidCookieLog = new UserDataHelper(log); private static final StringManager sm = StringManager.getManager("org.apache.tomcat.util.http.parser"); private static final boolean isCookieOctet[] = new boolean[256]; private static final boolean isText[] = new boolean[256]; private static final byte[] VERSION_BYTES = "$Version".getBytes(StandardCharsets.ISO_8859_1); private static final byte[] PATH_BYTES = "$Path".getBytes(StandardCharsets.ISO_8859_1); private static final byte[] DOMAIN_BYTES = "$Domain".getBytes(StandardCharsets.ISO_8859_1); private static final byte[] EMPTY_BYTES = new byte[0]; private static final byte TAB_BYTE = (byte) 0x09; private static final byte SPACE_BYTE = (byte) 0x20; private static final byte QUOTE_BYTE = (byte) 0x22; private static final byte COMMA_BYTE = (byte) 0x2C; private static final byte FORWARDSLASH_BYTE = (byte) 0x2F; private static final byte SEMICOLON_BYTE = (byte) 0x3B; private static final byte EQUALS_BYTE = (byte) 0x3D; private static final byte SLASH_BYTE = (byte) 0x5C; private static final byte DEL_BYTE = (byte) 0x7F; static { // %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E (RFC6265) // %x80 to %xFF (UTF-8) for (int i = 0; i < 256; i++) { if (i < 0x21 || i == QUOTE_BYTE || i == COMMA_BYTE || i == SEMICOLON_BYTE || i == SLASH_BYTE || i == DEL_BYTE) { isCookieOctet[i] = false; } else { isCookieOctet[i] = true; } } for (int i = 0; i < 256; i++) { if (i < TAB_BYTE || (i > TAB_BYTE && i < SPACE_BYTE) || i == DEL_BYTE) { isText[i] = false; } else { isText[i] = true; } } } private Cookie() { // Hide default constructor } public static void parseCookie(byte[] bytes, int offset, int len, ServerCookies serverCookies) { // ByteBuffer is used throughout this parser as it allows the byte[] // and position information to be easily passed between parsing methods ByteBuffer bb = new ByteBuffer(bytes, offset, len); // Using RFC6265 parsing rules, check to see if the header starts with a // version marker. An RFC2109 version marker may be read using RFC6265 // parsing rules. If version 1, use RFC2109. Else use RFC6265. skipLWS(bb); // Record position in case we need to return. int mark = bb.position(); SkipResult skipResult = skipBytes(bb, VERSION_BYTES); if (skipResult != SkipResult.FOUND) { // No need to reset position since skipBytes() will have done it parseCookieRfc6265(bb, serverCookies); return; } skipLWS(bb); skipResult = skipByte(bb, EQUALS_BYTE); if (skipResult != SkipResult.FOUND) { // Need to reset position as skipByte() will only have reset to // position before it was called bb.position(mark); parseCookieRfc6265(bb, serverCookies); return; } skipLWS(bb); ByteBuffer value = readCookieValue(bb); if (value != null && value.remaining() == 1) { int version = value.get() - '0'; if (version == 1 || version == 0) { // $Version=1 -> RFC2109 // $Version=0 -> RFC2109 skipLWS(bb); byte b = bb.get(); if (b == SEMICOLON_BYTE || b == COMMA_BYTE) { parseCookieRfc2109(bb, serverCookies, version); } } else { // Unrecognised version. // Ignore this header. value.rewind(); logInvalidVersion(value); } } else { // Unrecognised version. // Ignore this header. logInvalidVersion(value); } } public static String unescapeCookieValueRfc2109(String input) { if (input == null || input.length() < 2) { return input; } if (input.charAt(0) != '"' && input.charAt(input.length() - 1) != '"') { return input; } StringBuilder sb = new StringBuilder(input.length()); char[] chars = input.toCharArray(); boolean escaped = false; for (int i = 1; i < input.length() - 1; i++) { if (chars[i] == '\\') { escaped = true; } else if (escaped) { escaped = false; if (chars[i] < 128) { sb.append(chars[i]); } else { sb.append('\\'); sb.append(chars[i]); } } else { sb.append(chars[i]); } } return sb.toString(); } private static void parseCookieRfc6265(ByteBuffer bb, ServerCookies serverCookies) { boolean moreToProcess = true; while (moreToProcess) { skipLWS(bb); ByteBuffer name = readToken(bb); ByteBuffer value = null; skipLWS(bb); SkipResult skipResult = skipByte(bb, EQUALS_BYTE); if (skipResult == SkipResult.FOUND) { skipLWS(bb); value = readCookieValueRfc6265(bb); if (value == null) { logInvalidHeader(bb); // Invalid cookie value. Skip to the next semi-colon skipUntilSemiColon(bb); continue; } skipLWS(bb); } skipResult = skipByte(bb, SEMICOLON_BYTE); if (skipResult == SkipResult.FOUND) { // NO-OP } else if (skipResult == SkipResult.NOT_FOUND) { logInvalidHeader(bb); // Invalid cookie. Ignore it and skip to the next semi-colon skipUntilSemiColon(bb); continue; } else { // SkipResult.EOF moreToProcess = false; } if (name.hasRemaining()) { ServerCookie sc = serverCookies.addCookie(); sc.getName().setBytes(name.array(), name.position(), name.remaining()); if (value == null) { sc.getValue().setBytes(EMPTY_BYTES, 0, EMPTY_BYTES.length); } else { sc.getValue().setBytes(value.array(), value.position(), value.remaining()); } } } } private static void parseCookieRfc2109(ByteBuffer bb, ServerCookies serverCookies, int version) { boolean moreToProcess = true; while (moreToProcess) { skipLWS(bb); boolean parseAttributes = true; ByteBuffer name = readToken(bb); ByteBuffer value = null; ByteBuffer path = null; ByteBuffer domain = null; skipLWS(bb); SkipResult skipResult = skipByte(bb, EQUALS_BYTE); if (skipResult == SkipResult.FOUND) { skipLWS(bb); value = readCookieValueRfc2109(bb, false); if (value == null) { skipInvalidCookie(bb); continue; } skipLWS(bb); } skipResult = skipByte(bb, COMMA_BYTE); if (skipResult == SkipResult.FOUND) { parseAttributes = false; } else { skipResult = skipByte(bb, SEMICOLON_BYTE); } if (skipResult == SkipResult.EOF) { parseAttributes = false; moreToProcess = false; } else if (skipResult == SkipResult.NOT_FOUND) { skipInvalidCookie(bb); continue; } if (parseAttributes) { skipLWS(bb); skipResult = skipBytes(bb, PATH_BYTES); if (skipResult == SkipResult.FOUND) { skipLWS(bb); skipResult = skipByte(bb, EQUALS_BYTE); if (skipResult != SkipResult.FOUND) { skipInvalidCookie(bb); continue; } skipLWS(bb); path = readCookieValueRfc2109(bb, true); if (path == null) { skipInvalidCookie(bb); continue; } skipLWS(bb); skipResult = skipByte(bb, COMMA_BYTE); if (skipResult == SkipResult.FOUND) { parseAttributes = false; } else { skipResult = skipByte(bb, SEMICOLON_BYTE); } if (skipResult == SkipResult.EOF) { parseAttributes = false; moreToProcess = false; } else if (skipResult == SkipResult.NOT_FOUND) { skipInvalidCookie(bb); continue; } } } if (parseAttributes) { skipLWS(bb); skipResult = skipBytes(bb, DOMAIN_BYTES); if (skipResult == SkipResult.FOUND) { skipLWS(bb); skipResult = skipByte(bb, EQUALS_BYTE); if (skipResult != SkipResult.FOUND) { skipInvalidCookie(bb); continue; } skipLWS(bb); domain = readCookieValueRfc2109(bb, false); if (domain == null) { skipInvalidCookie(bb); continue; } skipLWS(bb); skipResult = skipByte(bb, COMMA_BYTE); if (skipResult == SkipResult.FOUND) { parseAttributes = false; } else { skipResult = skipByte(bb, SEMICOLON_BYTE); } if (skipResult == SkipResult.EOF) { parseAttributes = false; moreToProcess = false; } else if (skipResult == SkipResult.NOT_FOUND) { skipInvalidCookie(bb); continue; } } } if (name.hasRemaining() && value != null && value.hasRemaining()) { ServerCookie sc = serverCookies.addCookie(); sc.setVersion(version); sc.getName().setBytes(name.array(), name.position(), name.remaining()); sc.getValue().setBytes(value.array(), value.position(), value.remaining()); if (domain != null) { sc.getDomain().setBytes(domain.array(), domain.position(), domain.remaining()); } if (path != null) { sc.getPath().setBytes(path.array(), path.position(), path.remaining()); } } } } private static void skipInvalidCookie(ByteBuffer bb) { logInvalidHeader(bb); // Invalid cookie value. Skip to the next semi-colon skipUntilSemiColonOrComma(bb); } private static void skipLWS(ByteBuffer bb) { while(bb.hasRemaining()) { byte b = bb.get(); if (b != TAB_BYTE && b != SPACE_BYTE) { bb.rewind(); break; } } } private static void skipUntilSemiColon(ByteBuffer bb) { while(bb.hasRemaining()) { if (bb.get() == SEMICOLON_BYTE) { break; } } } private static void skipUntilSemiColonOrComma(ByteBuffer bb) { while(bb.hasRemaining()) { byte b = bb.get(); if (b == SEMICOLON_BYTE || b == COMMA_BYTE) { break; } } } private static SkipResult skipByte(ByteBuffer bb, byte target) { if (!bb.hasRemaining()) { return SkipResult.EOF; } if (bb.get() == target) { return SkipResult.FOUND; } bb.rewind(); return SkipResult.NOT_FOUND; } private static SkipResult skipBytes(ByteBuffer bb, byte[] target) { int mark = bb.position(); for (byte b : target) { if (!bb.hasRemaining()) { bb.position(mark); return SkipResult.EOF; } if (bb.get() != b) { bb.position(mark); return SkipResult.NOT_FOUND; } } return SkipResult.FOUND; }
Similar to readCookieValueRfc6265() but also allows a comma to terminate the value (as permitted by RFC2109).
/** * Similar to readCookieValueRfc6265() but also allows a comma to terminate * the value (as permitted by RFC2109). */
private static ByteBuffer readCookieValue(ByteBuffer bb) { boolean quoted = false; if (bb.hasRemaining()) { if (bb.get() == QUOTE_BYTE) { quoted = true; } else { bb.rewind(); } } int start = bb.position(); int end = bb.limit(); while (bb.hasRemaining()) { byte b = bb.get(); if (isCookieOctet[(b & 0xFF)]) { // NO-OP } else if (b == SEMICOLON_BYTE || b == COMMA_BYTE || b == SPACE_BYTE || b == TAB_BYTE) { end = bb.position() - 1; bb.position(end); break; } else if (quoted && b == QUOTE_BYTE) { end = bb.position() - 1; break; } else { // Invalid cookie return null; } } return new ByteBuffer(bb.bytes, start, end - start); }
Similar to readCookieValue() but treats a comma as part of an invalid value.
/** * Similar to readCookieValue() but treats a comma as part of an invalid * value. */
private static ByteBuffer readCookieValueRfc6265(ByteBuffer bb) { boolean quoted = false; if (bb.hasRemaining()) { if (bb.get() == QUOTE_BYTE) { quoted = true; } else { bb.rewind(); } } int start = bb.position(); int end = bb.limit(); while (bb.hasRemaining()) { byte b = bb.get(); if (isCookieOctet[(b & 0xFF)]) { // NO-OP } else if (b == SEMICOLON_BYTE || b == SPACE_BYTE || b == TAB_BYTE) { end = bb.position() - 1; bb.position(end); break; } else if (quoted && b == QUOTE_BYTE) { end = bb.position() - 1; break; } else { // Invalid cookie return null; } } return new ByteBuffer(bb.bytes, start, end - start); } private static ByteBuffer readCookieValueRfc2109(ByteBuffer bb, boolean allowForwardSlash) { if (!bb.hasRemaining()) { return null; } if (bb.peek() == QUOTE_BYTE) { return readQuotedString(bb); } else { if (allowForwardSlash) { return readTokenAllowForwardSlash(bb); } else { return readToken(bb); } } } private static ByteBuffer readToken(ByteBuffer bb) { final int start = bb.position(); int end = bb.limit(); while (bb.hasRemaining()) { if (!HttpParser.isToken(bb.get())) { end = bb.position() - 1; bb.position(end); break; } } return new ByteBuffer(bb.bytes, start, end - start); } private static ByteBuffer readTokenAllowForwardSlash(ByteBuffer bb) { final int start = bb.position(); int end = bb.limit(); while (bb.hasRemaining()) { byte b = bb.get(); if (b != FORWARDSLASH_BYTE && !HttpParser.isToken(b)) { end = bb.position() - 1; bb.position(end); break; } } return new ByteBuffer(bb.bytes, start, end - start); } private static ByteBuffer readQuotedString(ByteBuffer bb) { int start = bb.position(); // Read the opening quote bb.get(); boolean escaped = false; while (bb.hasRemaining()) { byte b = bb.get(); if (b == SLASH_BYTE) { // Escaping another character escaped = true; } else if (escaped && b > (byte) -1) { escaped = false; } else if (b == QUOTE_BYTE) { return new ByteBuffer(bb.bytes, start, bb.position() - start); } else if (isText[b & 0xFF]) { escaped = false; } else { return null; } } return null; } private static void logInvalidHeader(ByteBuffer bb) { UserDataHelper.Mode logMode = invalidCookieLog.getNextMode(); if (logMode != null) { String headerValue = new String(bb.array(), bb.position(), bb.limit() - bb.position(), StandardCharsets.UTF_8); String message = sm.getString("cookie.invalidCookieValue", headerValue); switch (logMode) { case INFO_THEN_DEBUG: message += sm.getString("cookie.fallToDebug"); //$FALL-THROUGH$ case INFO: log.info(message); break; case DEBUG: log.debug(message); } } } private static void logInvalidVersion(ByteBuffer value) { UserDataHelper.Mode logMode = invalidCookieVersionLog.getNextMode(); if (logMode != null) { String version; if (value == null) { version = sm.getString("cookie.valueNotPresent"); } else { version = new String(value.bytes, value.position(), value.limit() - value.position(), StandardCharsets.UTF_8); } String message = sm.getString("cookie.invalidCookieVersion", version); switch (logMode) { case INFO_THEN_DEBUG: message += sm.getString("cookie.fallToDebug"); //$FALL-THROUGH$ case INFO: log.info(message); break; case DEBUG: log.debug(message); } } }
Custom implementation that skips many of the safety checks in ByteBuffer.
/** * Custom implementation that skips many of the safety checks in * {@link java.nio.ByteBuffer}. */
private static class ByteBuffer { private final byte[] bytes; private int limit; private int position = 0; public ByteBuffer(byte[] bytes, int offset, int len) { this.bytes = bytes; this.position = offset; this.limit = offset + len; } public int position() { return position; } public void position(int position) { this.position = position; } public int limit() { return limit; } public int remaining() { return limit - position; } public boolean hasRemaining() { return position < limit; } public byte get() { return bytes[position++]; } public byte peek() { return bytes[position]; } public void rewind() { position--; } public byte[] array() { return bytes; } // For debug purposes @Override public String toString() { return "position [" + position + "], limit [" + limit + "]"; } } }