//
// ========================================================================
// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under
// the terms of the Eclipse Public License 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0
//
// This Source Code may also be made available under the following
// Secondary Licenses when the conditions for such availability set
// forth in the Eclipse Public License, v. 2.0 are satisfied:
// the Apache License v2.0 which is available at
// https://www.apache.org/licenses/LICENSE-2.0
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//

package org.eclipse.jetty.http;

import java.util.List;
import java.util.Locale;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static org.eclipse.jetty.http.CookieCompliance.Violation.COMMA_NOT_VALID_OCTET;
import static org.eclipse.jetty.http.CookieCompliance.Violation.RESERVED_NAMES_NOT_DOLLAR_PREFIXED;

Cookie parser
/** * Cookie parser */
public abstract class CookieCutter { protected static final Logger LOG = LoggerFactory.getLogger(CookieCutter.class); protected final CookieCompliance _complianceMode; private final ComplianceViolation.Listener _complianceListener; protected CookieCutter(CookieCompliance compliance, ComplianceViolation.Listener complianceListener) { _complianceMode = compliance; _complianceListener = complianceListener; } protected void parseFields(List<String> rawFields) { StringBuilder unquoted = null; // For each cookie field for (String hdr : rawFields) { // Parse the header String name = null; String cookieName = null; String cookieValue = null; String cookiePath = null; String cookieDomain = null; String cookieComment = null; int cookieVersion = 0; boolean invalue = false; boolean inQuoted = false; boolean quoted = false; boolean escaped = false; boolean reject = false; int tokenstart = -1; int tokenend = -1; for (int i = 0, length = hdr.length(); i <= length; i++) { char c = i == length ? 0 : hdr.charAt(i); // Handle quoted values for value if (inQuoted) { if (escaped) { escaped = false; if (c > 0) unquoted.append(c); else { unquoted.setLength(0); inQuoted = false; i--; } continue; } switch (c) { case '"': inQuoted = false; quoted = true; tokenstart = i; tokenend = -1; break; case '\\': escaped = true; continue; case 0: // unterminated quote, let's ignore quotes unquoted.setLength(0); inQuoted = false; i--; continue; default: unquoted.append(c); continue; } } else { // Handle name and value state machines if (invalue) { // parse the cookie-value switch (c) { case ' ': case '\t': break; case ',': if (COMMA_NOT_VALID_OCTET.isAllowedBy(_complianceMode)) reportComplianceViolation(COMMA_NOT_VALID_OCTET, "Cookie " + cookieName); else { if (quoted) { // must have been a bad internal quote. let's fix as best we can unquoted.append(hdr, tokenstart, i--); inQuoted = true; quoted = false; continue; } if (tokenstart < 0) tokenstart = i; tokenend = i; continue; } // fall through case 0: case ';': { String value; if (quoted) { value = unquoted.toString(); unquoted.setLength(0); quoted = false; } else if (tokenstart >= 0) value = tokenend >= tokenstart ? hdr.substring(tokenstart, tokenend + 1) : hdr.substring(tokenstart); else value = ""; try { if (name.startsWith("$")) { if (RESERVED_NAMES_NOT_DOLLAR_PREFIXED.isAllowedBy(_complianceMode)) { reportComplianceViolation(RESERVED_NAMES_NOT_DOLLAR_PREFIXED, "Cookie " + cookieName + " field " + name); String lowercaseName = name.toLowerCase(Locale.ENGLISH); switch (lowercaseName) { case "$path": cookiePath = value; break; case "$domain": cookieDomain = value; break; case "$port": cookieComment = "$port=" + value; break; case "$version": cookieVersion = Integer.parseInt(value); break; default: break; } } } else { // This is a new cookie, so add the completed last cookie if we have one if (cookieName != null) { if (!reject) { addCookie(cookieName, cookieValue, cookieDomain, cookiePath, cookieVersion, cookieComment); reject = false; } cookieDomain = null; cookiePath = null; cookieComment = null; } cookieName = name; cookieValue = value; } } catch (Exception e) { LOG.debug("Unable to process Cookie", e); } name = null; tokenstart = -1; invalue = false; break; } case '"': if (tokenstart < 0) { tokenstart = i; inQuoted = true; if (unquoted == null) unquoted = new StringBuilder(); break; } // fall through to default case default: if (quoted) { // must have been a bad internal quote. let's fix as best we can unquoted.append(hdr, tokenstart, i--); inQuoted = true; quoted = false; continue; } if (_complianceMode == CookieCompliance.RFC6265) { if (isRFC6265RejectedCharacter(inQuoted, c)) { reject = true; } } if (tokenstart < 0) tokenstart = i; tokenend = i; continue; } } else { // parse the cookie-name switch (c) { case 0: case ' ': case '\t': continue; case '"': // Quoted name is not allowed in any version of the Cookie spec reject = true; break; case ';': // a cookie terminated with no '=' sign. tokenstart = -1; invalue = false; reject = false; continue; case '=': if (quoted) { name = unquoted.toString(); unquoted.setLength(0); quoted = false; } else if (tokenstart >= 0) name = tokenend >= tokenstart ? hdr.substring(tokenstart, tokenend + 1) : hdr.substring(tokenstart); tokenstart = -1; invalue = true; break; default: if (quoted) { // must have been a bad internal quote. let's fix as best we can unquoted.append(hdr, tokenstart, i--); inQuoted = true; quoted = false; continue; } if (_complianceMode == CookieCompliance.RFC6265) { if (isRFC6265RejectedCharacter(inQuoted, c)) { reject = true; } } if (tokenstart < 0) tokenstart = i; tokenend = i; continue; } } } } if (cookieName != null && !reject) addCookie(cookieName, cookieValue, cookieDomain, cookiePath, cookieVersion, cookieComment); } } protected void reportComplianceViolation(CookieCompliance.Violation violation, String reason) { if (_complianceListener != null) { _complianceListener.onComplianceViolation(_complianceMode, violation, reason); } } protected abstract void addCookie(String cookieName, String cookieValue, String cookieDomain, String cookiePath, int cookieVersion, String cookieComment); protected boolean isRFC6265RejectedCharacter(boolean inQuoted, char c) { if (inQuoted) { // We only reject if a Control Character is encountered if (Character.isISOControl(c)) { return true; } } else { /* From RFC6265 - Section 4.1.1 - Syntax * cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E * ; US-ASCII characters excluding CTLs, * ; whitespace DQUOTE, comma, semicolon, * ; and backslash */ return Character.isISOControl(c) || // control characters c > 127 || // 8-bit characters c == ',' || // comma c == ';'; // semicolon } return false; } }