Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.TreeSet;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
Expand Down Expand Up @@ -79,9 +80,29 @@ private void readObject(final ObjectInputStream stream) throws IOException, Clas
*/
@Override
public void addCookie(final Cookie cookie) {
addCookie(cookie, true);
}

/**
* Adds an {@link Cookie HTTP cookie} received over a connection whose security is described by
* {@code secureConnection}, replacing any existing equivalent cookie. A cookie received over a
* non-secure connection does not replace an existing secure cookie of the same identity. If the
* given cookie has already expired it will not be added, but an existing equivalent cookie will
* still be removed.
*
* @param cookie the {@link Cookie cookie} to be added
* @param secureConnection whether the cookie was received over a secure connection
*
* @since 5.7
*/
@Override
public void addCookie(final Cookie cookie, final boolean secureConnection) {
if (cookie != null) {
lock.writeLock().lock();
try {
if (!secureConnection && overlaysSecureCookie(cookie)) {
return;
}
final Cookie oldCookie = cookies.ceiling(cookie);
if (oldCookie != null && CookieIdentityComparator.INSTANCE.compare(oldCookie, cookie) == 0) {
if (cookie instanceof SetCookie) {
Expand All @@ -103,6 +124,48 @@ public void addCookie(final Cookie cookie) {
}
}

private boolean overlaysSecureCookie(final Cookie cookie) {
for (final Cookie existing : cookies) {
if (existing.isSecure()
&& namesMatch(existing.getName(), cookie.getName())
&& (domainMatch(cookie.getDomain(), existing.getDomain())
|| domainMatch(existing.getDomain(), cookie.getDomain()))
&& pathMatch(cookie.getPath(), existing.getPath())) {
return true;
}
}
return false;
}

private static boolean namesMatch(final String a, final String b) {
return a == null ? b == null : a.equals(b);
}

private static boolean domainMatch(final String host, final String domain) {
if (host == null || domain == null) {
return false;
}
final String h = host.toLowerCase(Locale.ROOT);
String d = domain.toLowerCase(Locale.ROOT);
if (d.startsWith(".")) {
d = d.substring(1);
}
return h.equals(d)
|| h.length() > d.length() && h.endsWith(d) && h.charAt(h.length() - d.length() - 1) == '.';
}

private static boolean pathMatch(final String path, final String cookiePath) {
final String p = path == null ? "/" : path;
final String cp = cookiePath == null ? "/" : cookiePath;
if (p.equals(cp)) {
return true;
}
if (p.startsWith(cp)) {
return cp.endsWith("/") || p.charAt(cp.length()) == '/';
}
return false;
}

/**
* Adds an array of {@link Cookie HTTP cookies}. Cookies are added individually and
* in the given array order. If any of the given cookies has already expired it will
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ public interface Cookie {
String EXPIRES_ATTR = "expires";
String HTTP_ONLY_ATTR = "httponly";

/**
* @since 5.7
*/
String SAME_SITE_ATTR = "samesite";

/**
* @since 5.0
*/
Expand Down Expand Up @@ -181,5 +186,25 @@ default boolean isHttpOnly() {
return false;
}

/**
* Returns the value of the {@code SameSite} attribute, or {@code null} if the attribute is
* absent or unrecognized.
*
* @since 5.7
*/
default SameSite getSameSite() {
return SameSite.fromString(getAttribute(SAME_SITE_ATTR));
}

/**
* Indicates whether this cookie is host-only, meaning it was set without a {@code Domain}
* attribute and therefore applies only to the exact host that set it.
*
* @since 5.7
*/
default boolean isHostOnly() {
return !containsAttribute(DOMAIN_ATTR);
}

}

Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ public int compare(final Cookie c1, final Cookie c2) {
}
res = p1.compareTo(p2);
}
if (res == 0) {
res = Boolean.compare(c1.isHostOnly(), c2.isHostOnly());
}
return res;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,20 @@ public interface CookieStore {
*/
void addCookie(Cookie cookie);

/**
* Adds an {@link Cookie} received over a connection whose security is described by
* {@code secureConnection}, replacing any existing equivalent cookie. A cookie received over a
* non-secure connection must not replace an existing secure cookie of the same identity. The
* default implementation ignores the security context and delegates to {@link #addCookie(Cookie)}.
*
* @param cookie the {@link Cookie cookie} to be added
* @param secureConnection whether the cookie was received over a secure connection
* @since 5.7
*/
default void addCookie(final Cookie cookie, final boolean secureConnection) {
addCookie(cookie);
}

/**
* Returns all cookies contained in this store.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
* ====================================================================
* 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 software consists of voluntary contributions made by many
* individuals on behalf of the Apache Software Foundation. For more
* information on the Apache Software Foundation, please see
* <http://www.apache.org/>.
*
*/

package org.apache.hc.client5.http.cookie;

import java.util.Locale;

/**
* Enumeration of the values of the {@code SameSite} cookie attribute.
*
* @since 5.7
*/
public enum SameSite {

/**
* The cookie is only sent for same-site requests.
*/
STRICT("Strict"),

/**
* The cookie is sent for same-site requests and top-level cross-site navigations.
*/
LAX("Lax"),

/**
* The cookie is sent for all requests. A cookie with this value must also be secure.
*/
NONE("None");

private final String attributeValue;

SameSite(final String attributeValue) {
this.attributeValue = attributeValue;
}

/**
* Returns the canonical attribute value as it appears in a {@code Set-Cookie} header.
*/
public String getAttributeValue() {
return attributeValue;
}

/**
* Resolves a {@code SameSite} value from a raw attribute value using a case-insensitive match,
* returning {@code null} when the value is absent or unrecognized.
*/
public static SameSite fromString(final String value) {
if (value == null) {
return null;
}
switch (value.trim().toLowerCase(Locale.ROOT)) {
case "strict":
return STRICT;
case "lax":
return LAX;
case "none":
return NONE;
default:
return null;
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ public void parse(final SetCookie cookie, final String value)
throw new MalformedCookieException("Invalid 'expires' attribute: "
+ value);
}
cookie.setExpiryDate(expiry);
cookie.setExpiryDate(CookieExpiryPolicy.cap(expiry, Instant.now()));
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
*/
package org.apache.hc.client5.http.impl.cookie;

import java.math.BigInteger;
import java.time.Instant;

import org.apache.hc.client5.http.cookie.CommonCookieAttributeHandler;
Expand Down Expand Up @@ -62,19 +63,19 @@ public void parse(final SetCookie cookie, final String value)
if (value == null) {
throw new MalformedCookieException("Missing value for 'max-age' attribute");
}
final int age;
final BigInteger age;
try {
age = Integer.parseInt(value);
age = new BigInteger(value);
} catch (final NumberFormatException e) {
throw new MalformedCookieException ("Invalid 'max-age' attribute: "
+ value);
throw new MalformedCookieException("Invalid 'max-age' attribute: " + value);
}
if (age <= 0) {
if (age.signum() <= 0) {
// RFC 6265 user-agent processing: delta-seconds <= 0 means immediate expiry.
cookie.setExpiryDate(Instant.EPOCH);
return;
}
cookie.setExpiryDate(Instant.now().plusSeconds(age));
final BigInteger maxSeconds = BigInteger.valueOf(CookieExpiryPolicy.MAX_LIFETIME.getSeconds());
cookie.setExpiryDate(Instant.now().plusSeconds(age.min(maxSeconds).longValueExact()));
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* ====================================================================
* 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 software consists of voluntary contributions made by many
* individuals on behalf of the Apache Software Foundation. For more
* information on the Apache Software Foundation, please see
* <http://www.apache.org/>.
*
*/

package org.apache.hc.client5.http.impl.cookie;

import org.apache.hc.client5.http.cookie.CommonCookieAttributeHandler;
import org.apache.hc.client5.http.cookie.Cookie;
import org.apache.hc.client5.http.cookie.CookieOrigin;
import org.apache.hc.client5.http.cookie.MalformedCookieException;
import org.apache.hc.client5.http.cookie.SameSite;
import org.apache.hc.client5.http.cookie.SetCookie;
import org.apache.hc.core5.annotation.Contract;
import org.apache.hc.core5.annotation.ThreadingBehavior;
import org.apache.hc.core5.util.Args;

/**
* Cookie {@code SameSite} attribute handler. The raw attribute value is retained by the cookie
* specification and exposed through {@link Cookie#getSameSite()}; this handler enforces that a
* {@code SameSite=None} cookie must also be secure.
*
* @since 5.7
*/
@Contract(threading = ThreadingBehavior.STATELESS)
public class BasicSameSiteHandler extends AbstractCookieAttributeHandler implements CommonCookieAttributeHandler {

/**
* Default instance of {@link BasicSameSiteHandler}.
*/
public static final BasicSameSiteHandler INSTANCE = new BasicSameSiteHandler();

public BasicSameSiteHandler() {
super();
}

@Override
public void parse(final SetCookie cookie, final String value) throws MalformedCookieException {
Args.notNull(cookie, "Cookie");
}

@Override
public void validate(final Cookie cookie, final CookieOrigin origin) throws MalformedCookieException {
Args.notNull(cookie, "Cookie");
if (SameSite.NONE == cookie.getSameSite() && !cookie.isSecure()) {
throw new MalformedCookieException("Cookie '" + cookie.getName()
+ "' has SameSite=None but is not marked secure");
}
}

@Override
public String getAttributeName() {
return Cookie.SAME_SITE_ATTR;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,17 @@ public void parse(final SetCookie cookie, final String value)
cookie.setSecure(true);
}

@Override
public void validate(final Cookie cookie, final CookieOrigin origin)
throws MalformedCookieException {
Args.notNull(cookie, "Cookie");
Args.notNull(origin, "Cookie origin");
if (cookie.isSecure() && !origin.isSecure()) {
throw new MalformedCookieException("Cookie '" + cookie.getName()
+ "' is marked secure but was received over a non-secure connection");
}
}

@Override
public boolean match(final Cookie cookie, final CookieOrigin origin) {
Args.notNull(cookie, "Cookie");
Expand Down
Loading
Loading