All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.eclipse.jetty.http.HttpCookieStore Maven / Gradle / Ivy

//
// ========================================================================
// 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.http;

import java.net.URI;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import org.eclipse.jetty.util.NanoTime;
import org.eclipse.jetty.util.thread.AutoLock;

/**
 * 

A container for {@link HttpCookie}s.

*

HTTP cookies are stored along with a {@link URI} via {@link #add(URI, HttpCookie)} * and retrieved via {@link #match(URI)}, which implements the path matching algorithm * defined by RFC 6265.

*/ public interface HttpCookieStore { /** *

Adds a cookie to this store, if possible.

*

The cookie may not be added for various reasons; for example, * it may be already expired, or its domain attribute does not * match that of the URI, etc.

*

The cookie is associated with the given {@code URI}, so that * a call to {@link #match(URI)} returns the cookie only if the * URIs match.

* * @param uri the {@code URI} associated with the cookie * @param cookie the cookie to add * @return whether the cookie has been added */ public boolean add(URI uri, HttpCookie cookie); /** * @return all the cookies */ public List all(); /** *

Returns the cookies that match the given {@code URI}.

* * @param uri the {@code URI} to match against * @return a list of cookies that match the given {@code URI} */ public List match(URI uri); /** *

Removes the cookie associated with the given {@code URI}.

* * @param uri the {@code URI} associated with the cookie to remove * @param cookie the cookie to remove * @return whether the cookie has been removed */ public boolean remove(URI uri, HttpCookie cookie); /** *

Removes all the cookies from this store.

* * @return whether the store modified by this call */ public boolean clear(); /** *

An implementation of {@link HttpCookieStore} that does not store any cookie.

*/ public static class Empty implements HttpCookieStore { @Override public boolean add(URI uri, HttpCookie cookie) { return false; } @Override public List all() { return List.of(); } @Override public List match(URI uri) { return List.of(); } @Override public boolean remove(URI uri, HttpCookie cookie) { return false; } @Override public boolean clear() { return false; } } /** *

A default implementation of {@link HttpCookieStore}.

*/ public static class Default implements HttpCookieStore { private final AutoLock lock = new AutoLock(); private final Map> cookies = new HashMap<>(); @Override public boolean add(URI uri, HttpCookie cookie) { // TODO: reject if cookie size is too big? boolean secure = HttpScheme.isSecure(uri.getScheme()); // Do not accept a secure cookie sent over an insecure channel. if (cookie.isSecure() && !secure) return false; String cookieDomain = cookie.getDomain(); if (cookieDomain != null) { cookieDomain = cookieDomain.toLowerCase(Locale.ENGLISH); if (cookieDomain.startsWith(".")) cookieDomain = cookieDomain.substring(1); // RFC 6265 section 4.1.2.3, ignore Domain if ends with ".". if (cookieDomain.endsWith(".")) cookieDomain = uri.getHost(); // Reject top-level domains. // TODO: should also reject "top" domain such as co.uk, gov.au, etc. if (!cookieDomain.contains(".")) { if (!cookieDomain.equals("localhost")) return false; } String domain = uri.getHost(); if (domain != null) { domain = domain.toLowerCase(Locale.ENGLISH); // If uri.host==foo.example.com, only accept // cookie.domain==(foo.example.com|example.com). if (!domain.endsWith(cookieDomain)) return false; int beforeMatch = domain.length() - cookieDomain.length() - 1; if (beforeMatch >= 0 && domain.charAt(beforeMatch) != '.') return false; } } else { // No explicit cookie domain, use the origin domain. cookieDomain = uri.getHost(); } // Cookies are stored under their domain, so that: // - add(sub.example.com, cookie[Domain]=null) => Key[domain=sub.example.com] // - add(sub.example.com, cookie[Domain]=example.com) => Key[domain=example.com] // This facilitates the matching algorithm. Key key = new Key(secure, cookieDomain); boolean[] result = new boolean[]{true}; try (AutoLock ignored = lock.lock()) { cookies.compute(key, (k, v) -> { // RFC 6265, section 4.1.2. // Evict an existing cookie with // same name, domain and path. if (v != null) v.remove(cookie); // Add only non-expired cookies. if (cookie.isExpired()) { result[0] = false; return v == null || v.isEmpty() ? null : v; } if (v == null) v = new ArrayList<>(); v.add(new Cookie(cookie)); return v; }); } return result[0]; } @Override public List all() { try (AutoLock ignored = lock.lock()) { return cookies.values().stream() .flatMap(Collection::stream) .toList(); } } @Override public List match(URI uri) { List result = new ArrayList<>(); boolean secure = HttpScheme.isSecure(uri.getScheme()); String uriDomain = uri.getHost(); String path = uri.getPath(); if (path == null || path.trim().isEmpty()) path = "/"; try (AutoLock ignored = lock.lock()) { // Given the way cookies are stored in the Map, the matching // algorithm starts with the URI domain and iterates chopping // its subdomains, accumulating the results. // For example, for uriDomain = sub.example.com, the cookies // Map is accessed with the following Keys: // - Key[domain=sub.example.com] // - chop domain to example.com // - Key[domain=example.com] // - chop domain to com // invalid domain, exit iteration. String domain = uriDomain; while (true) { Key key = new Key(secure, domain); List stored = cookies.get(key); Iterator iterator = stored == null ? Collections.emptyIterator() : stored.iterator(); while (iterator.hasNext()) { HttpCookie cookie = iterator.next(); // Check and remove expired cookies. if (cookie.isExpired()) { iterator.remove(); continue; } // Check whether the cookie is secure. if (cookie.isSecure() && !secure) continue; // Match the domain. if (!domainMatches(uriDomain, key.domain, cookie.getDomain())) continue; // Match the path. if (!pathMatches(path, cookie.getPath())) continue; result.add(cookie); } int dot = domain.indexOf('.'); if (dot < 0) break; // Remove one subdomain. domain = domain.substring(dot + 1); // Exit if the top-level domain was reached. if (domain.indexOf('.') < 0) break; } } return result; } private static boolean domainMatches(String uriDomain, String domain, String cookieDomain) { if (uriDomain == null) return true; if (domain != null) domain = domain.toLowerCase(Locale.ENGLISH); uriDomain = uriDomain.toLowerCase(Locale.ENGLISH); if (cookieDomain != null) cookieDomain = cookieDomain.toLowerCase(Locale.ENGLISH); if (cookieDomain == null || cookieDomain.endsWith(".")) { // RFC 6265, section 4.1.2.3. // No cookie domain -> cookie sent only to origin server. return uriDomain.equals(domain); } if (cookieDomain.startsWith(".")) cookieDomain = cookieDomain.substring(1); if (uriDomain.endsWith(cookieDomain)) { // The domain is the same as, or a subdomain of, the cookie domain. int beforeMatch = uriDomain.length() - cookieDomain.length() - 1; // Domains are the same. if (beforeMatch == -1) return true; // Verify it is a proper subdomain such as bar.foo.com, // not just a suffix of a domain such as bazfoo.com. return uriDomain.charAt(beforeMatch) == '.'; } return false; } private static boolean pathMatches(String path, String cookiePath) { if (cookiePath == null) return true; // RFC 6265, section 5.1.4, path matching algorithm. if (path.equals(cookiePath)) return true; if (path.startsWith(cookiePath)) return cookiePath.endsWith("/") || path.charAt(cookiePath.length()) == '/'; return false; } @Override public boolean remove(URI uri, HttpCookie cookie) { Key key = new Key(HttpScheme.isSecure(uri.getScheme()), uri.getHost()); try (AutoLock ignored = lock.lock()) { boolean[] result = new boolean[1]; cookies.compute(key, (k, v) -> { if (v == null) return null; boolean removed = v.remove(cookie); result[0] = removed; return v.isEmpty() ? null : v; }); return result[0]; } } @Override public boolean clear() { try (AutoLock ignored = lock.lock()) { if (cookies.isEmpty()) return false; cookies.clear(); return true; } } private record Key(boolean secure, String domain) { private Key(boolean secure, String domain) { this.secure = secure; this.domain = domain.toLowerCase(Locale.ENGLISH); } } private static class Cookie extends HttpCookie.Wrapper { private final long creationNanoTime = NanoTime.now(); public Cookie(HttpCookie wrapped) { super(wrapped); } @Override public boolean isExpired() { long maxAge = getMaxAge(); if (maxAge >= 0 && NanoTime.secondsSince(creationNanoTime) > maxAge) return true; Instant expires = getExpires(); return expires != null && Instant.now().isAfter(expires); } } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy