org.eclipse.jetty.server.HttpCookieUtils Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of jetty-server Show documentation
Show all versions of jetty-server Show documentation
The core jetty server artifact.
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.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.server;
import java.time.Instant;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import org.eclipse.jetty.http.ComplianceViolationException;
import org.eclipse.jetty.http.CookieCompliance;
import org.eclipse.jetty.http.HttpCookie;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.Syntax;
import org.eclipse.jetty.util.Attributes;
import org.eclipse.jetty.util.Index;
import org.eclipse.jetty.util.QuotedStringTokenizer;
/**
* Utility methods for server-side HTTP cookie handling.
*/
public final class HttpCookieUtils
{
/**
* Name of context attribute with default SameSite cookie value
*/
public static final String SAME_SITE_DEFAULT_ATTRIBUTE = "org.eclipse.jetty.cookie.sameSiteDefault";
private static final Index KNOWN_ATTRIBUTES = new Index.Builder().caseSensitive(false)
.with(HttpCookie.COMMENT_ATTRIBUTE)
.with(HttpCookie.DOMAIN_ATTRIBUTE)
.with(HttpCookie.EXPIRES_ATTRIBUTE)
.with(HttpCookie.HTTP_ONLY_ATTRIBUTE)
.with(HttpCookie.MAX_AGE_ATTRIBUTE)
.with(HttpCookie.PATH_ATTRIBUTE)
.with(HttpCookie.SAME_SITE_ATTRIBUTE)
.with(HttpCookie.SECURE_ATTRIBUTE)
.with(HttpCookie.PARTITIONED_ATTRIBUTE)
.build();
// RFC 1123 format of epoch for the Expires attribute.
private static final String EPOCH_EXPIRES = "Thu, 01 Jan 1970 00:00:00 GMT";
/**
* Check that samesite is set on the cookie. If not, use a
* context default value, if one has been set.
*
* @param cookie the cookie to check
* @param attributes the context to check settings
* @return either the original cookie, or a new one that has the samesit default set
*/
public static HttpCookie checkSameSite(HttpCookie cookie, Attributes attributes)
{
if (cookie == null || cookie.getSameSite() != null)
return cookie;
//sameSite is not set, use the default configured for the context, if one exists
HttpCookie.SameSite contextDefault = getSameSiteDefault(attributes);
if (contextDefault == null)
return cookie; //no default set
return HttpCookie.from(cookie, HttpCookie.SAME_SITE_ATTRIBUTE, contextDefault.getAttributeValue());
}
/**
* Get the default value for SameSite cookie attribute, if one
* has been set for the given context.
*
* @param contextAttributes the context to check for default SameSite value
* @return the default SameSite value or null if one does not exist
* @throws IllegalStateException if the default value is not a permitted value
*/
public static HttpCookie.SameSite getSameSiteDefault(Attributes contextAttributes)
{
if (contextAttributes == null)
return null;
Object o = contextAttributes.getAttribute(SAME_SITE_DEFAULT_ATTRIBUTE);
if (o == null)
return null;
if (o instanceof HttpCookie.SameSite)
return (HttpCookie.SameSite)o;
try
{
HttpCookie.SameSite samesite = Enum.valueOf(HttpCookie.SameSite.class, o.toString().trim().toUpperCase(Locale.ENGLISH));
contextAttributes.setAttribute(SAME_SITE_DEFAULT_ATTRIBUTE, samesite);
return samesite;
}
catch (Exception e)
{
throw new IllegalStateException(e);
}
}
public static String getSetCookie(HttpCookie httpCookie, CookieCompliance compliance)
{
if (compliance == null || CookieCompliance.RFC6265_LEGACY.compliesWith(compliance))
return getRFC6265SetCookie(httpCookie);
return getRFC2965SetCookie(httpCookie);
}
public static String getRFC2965SetCookie(HttpCookie httpCookie)
{
// Check arguments
String name = httpCookie.getName();
if (name == null || name.length() == 0)
throw new IllegalArgumentException("Invalid cookie name");
StringBuilder builder = new StringBuilder();
quoteIfNeededAndAppend(name, builder);
builder.append('=');
String value = httpCookie.getValue();
quoteIfNeededAndAppend(value, builder);
// Look for domain and path fields and check if they need to be quoted.
String domain = httpCookie.getDomain();
boolean hasDomain = domain != null && domain.length() > 0;
boolean quoteDomain = hasDomain && isQuoteNeeded(domain);
String path = httpCookie.getPath();
boolean hasPath = path != null && path.length() > 0;
boolean quotePath = hasPath && isQuoteNeeded(path);
// Upgrade the version if we have a comment or we need to quote value/path/domain or if they were already quoted
int version = httpCookie.getVersion();
String comment = httpCookie.getComment();
if (version == 0 && (comment != null || isQuoteNeeded(name) || isQuoteNeeded(value) || quoteDomain || quotePath ||
QuotedStringTokenizer.isQuoted(name) || QuotedStringTokenizer.isQuoted(value) ||
QuotedStringTokenizer.isQuoted(path) || QuotedStringTokenizer.isQuoted(domain)))
version = 1;
if (version == 1)
builder.append(";Version=1");
else if (version > 1)
builder.append(";Version=").append(version);
if (hasDomain)
{
builder.append(";Domain=");
if (quoteDomain)
HttpField.PARAMETER_TOKENIZER.quote(builder, domain);
else
builder.append(domain);
}
if (hasPath)
{
builder.append(";Path=");
if (quotePath)
HttpField.PARAMETER_TOKENIZER.quote(builder, path);
else
builder.append(path);
}
// Handle max-age and/or expires
long maxAge = httpCookie.getMaxAge();
if (maxAge >= 0)
{
// Always add the Expires attribute too, as some
// browsers do not handle max-age even with v1 cookies.
builder.append(";Expires=");
if (maxAge == 0)
builder.append(EPOCH_EXPIRES);
else
builder.append(HttpCookie.formatExpires(Instant.now().plusSeconds(maxAge)));
builder.append(";Max-Age=");
builder.append(maxAge);
}
if (httpCookie.isSecure())
builder.append(";Secure");
if (httpCookie.isHttpOnly())
builder.append(";HttpOnly");
if (httpCookie.isPartitioned())
builder.append(";Partitioned");
HttpCookie.SameSite sameSite = httpCookie.getSameSite();
if (sameSite != null)
builder.append(";SameSite=").append(sameSite.getAttributeValue());
if (comment != null)
{
builder.append(";Comment=");
quoteIfNeededAndAppend(comment, builder);
}
return builder.toString();
}
public static String getRFC6265SetCookie(HttpCookie httpCookie)
{
// Check arguments
String name = httpCookie.getName();
if (name == null || name.length() == 0)
throw new IllegalArgumentException("Bad cookie name");
try
{
// Name is checked for legality by servlet spec, but can also be passed directly so check again for quoting
// Per RFC6265, Cookie.name follows RFC2616 Section 2.2 token rules
Syntax.requireValidRFC2616Token(name, "RFC6265 Cookie name");
}
catch (IllegalArgumentException e)
{
throw new ComplianceViolationException(CookieCompliance.RFC6265, CookieCompliance.Violation.INVALID_COOKIES, "RFC6265 Cookie name must be a valid RFC2616 Token");
}
// Ensure that Per RFC6265, Cookie.value follows syntax rules
String value = httpCookie.getValue();
Syntax.requireValidRFC6265CookieValue(value);
// Format value and params
StringBuilder builder = new StringBuilder();
builder.append(name).append('=').append(value == null ? "" : value);
// Append path
String path = httpCookie.getPath();
if (path != null && path.length() > 0)
builder.append("; Path=").append(path);
// Append domain
String domain = httpCookie.getDomain();
if (domain != null && domain.length() > 0)
builder.append("; Domain=").append(domain);
// Handle max-age and/or expires
long maxAge = httpCookie.getMaxAge();
if (maxAge >= 0)
{
// Always use expires
// This is required as some browser (M$ this means you!) don't handle max-age even with v1 cookies
builder.append("; Expires=");
if (maxAge == 0)
builder.append(EPOCH_EXPIRES);
else
builder.append(HttpCookie.formatExpires(Instant.now().plusSeconds(maxAge)));
builder.append("; Max-Age=");
builder.append(maxAge);
}
// add the other fields
if (httpCookie.isSecure())
builder.append("; Secure");
if (httpCookie.isHttpOnly())
builder.append("; HttpOnly");
if (httpCookie.isPartitioned())
builder.append("; Partitioned");
Map attributes = httpCookie.getAttributes();
String sameSiteAttr = attributes.get(HttpCookie.SAME_SITE_ATTRIBUTE);
if (sameSiteAttr != null)
{
builder.append("; SameSite=");
builder.append(sameSiteAttr);
}
else
{
HttpCookie.SameSite sameSite = httpCookie.getSameSite();
if (sameSite != null)
{
builder.append("; SameSite=");
builder.append(sameSite.getAttributeValue());
}
}
//Add all other attributes
for (Map.Entry e : attributes.entrySet())
{
if (KNOWN_ATTRIBUTES.contains(e.getKey()))
continue;
builder.append("; ").append(e.getKey()).append("=");
builder.append(e.getValue());
}
return builder.toString();
}
/**
* Whether a cookie name/value/attribute needs to be quoted.
*
* @param text the text to check
* @return whether the text needs to be quoted
* @throws IllegalArgumentException if the text contains illegal characters
*/
private static boolean isQuoteNeeded(String text)
{
if (text == null || text.length() == 0)
return true;
if (QuotedStringTokenizer.isQuoted(text))
return false;
for (int i = 0; i < text.length(); i++)
{
char c = text.charAt(i);
if ("\",;\\ \t".indexOf(c) >= 0)
return true;
if (c < 0x20 || c >= 0x7F)
throw new IllegalArgumentException("Illegal character in cookie value");
}
return false;
}
/**
* Check if the Set-Cookie header represented as a string is for the name, domain and path given.
*
* @param setCookieHeader a Set-Cookie header
* @param name the cookie name to check
* @param domain the cookie domain to check
* @param path the cookie path to check
* @return true if all of the name, domain and path match the Set-Cookie header, false otherwise
*/
public static boolean match(String setCookieHeader, String name, String domain, String path)
{
//Parse the bare minimum
List cookies = java.net.HttpCookie.parse(setCookieHeader);
if (cookies.size() != 1)
return false;
java.net.HttpCookie cookie = cookies.get(0);
return match(cookie.getName(), cookie.getDomain(), cookie.getPath(), name, domain, path);
}
/**
* Check if the HttpCookie is for the given name, domain and path.
*
* @param cookie the jetty HttpCookie to check
* @param name the cookie name to check
* @param domain the cookie domain to check
* @param path the cookie path to check
* @return true if name, domain, and path, match all match the HttpCookie, false otherwise
*/
public static boolean match(HttpCookie cookie, String name, String domain, String path)
{
if (cookie == null)
return false;
return match(cookie.getName(), cookie.getDomain(), cookie.getPath(), name, domain, path);
}
/**
* Check if all old parameters match the new parameters.
*
* @return true if old and new names match exactly and the old and new domains match case-insensitively and the paths match exactly
*/
private static boolean match(String oldName, String oldDomain, String oldPath, String newName, String newDomain, String newPath)
{
if (oldName == null)
{
if (newName != null)
return false;
}
else if (!oldName.equals(newName))
return false;
if (oldDomain == null)
{
if (newDomain != null)
return false;
}
else if (!oldDomain.equalsIgnoreCase(newDomain))
return false;
if (oldPath == null)
return newPath == null;
return oldPath.equals(newPath);
}
private static void quoteIfNeededAndAppend(String text, StringBuilder builder)
{
if (isQuoteNeeded(text))
HttpField.PARAMETER_TOKENIZER.quote(builder, text);
else
builder.append(text);
}
private HttpCookieUtils()
{
}
/**
* A {@link HttpField} that holds an {@link HttpHeader#SET_COOKIE} as a
* {@link HttpCookie} instance, delaying any value generation until
* {@link #getValue()} is called.
*/
public static class SetCookieHttpField extends HttpField
{
private final HttpCookie _cookie;
private final String _value;
public SetCookieHttpField(HttpCookie cookie, CookieCompliance compliance)
{
super(HttpHeader.SET_COOKIE, (String)null);
this._cookie = cookie;
// trigger compliance check
this._value = getSetCookie(this._cookie, compliance);
}
public HttpCookie getHttpCookie()
{
return _cookie;
}
@Override
public String getValue()
{
return this._value;
}
}
}