diff --git a/client/src/main/java/org/asynchttpclient/AsyncHttpClientConfig.java b/client/src/main/java/org/asynchttpclient/AsyncHttpClientConfig.java index bfb1466d17..53e9d12696 100644 --- a/client/src/main/java/org/asynchttpclient/AsyncHttpClientConfig.java +++ b/client/src/main/java/org/asynchttpclient/AsyncHttpClientConfig.java @@ -20,6 +20,7 @@ import org.asynchttpclient.channel.ChannelPool; import org.asynchttpclient.channel.KeepAliveStrategy; +import org.asynchttpclient.cookie.CookieStore; import org.asynchttpclient.filter.IOExceptionFilter; import org.asynchttpclient.filter.RequestFilter; import org.asynchttpclient.filter.ResponseFilter; @@ -182,6 +183,13 @@ public interface AsyncHttpClientConfig { */ List getIoExceptionFilters(); + /** + * Return cookie store that is used to store and retrieve cookies + * + * @return {@link CookieStore} object + */ + CookieStore getCookieStore(); + /** * Return the number of time the library will retry when an {@link java.io.IOException} is throw by the remote server * diff --git a/client/src/main/java/org/asynchttpclient/DefaultAsyncHttpClient.java b/client/src/main/java/org/asynchttpclient/DefaultAsyncHttpClient.java index bd90c0e460..7c1bf23847 100644 --- a/client/src/main/java/org/asynchttpclient/DefaultAsyncHttpClient.java +++ b/client/src/main/java/org/asynchttpclient/DefaultAsyncHttpClient.java @@ -18,9 +18,11 @@ import static org.asynchttpclient.util.Assertions.assertNotNull; +import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Predicate; +import io.netty.handler.codec.http.cookie.Cookie; import org.asynchttpclient.channel.ChannelPool; import org.asynchttpclient.filter.FilterContext; import org.asynchttpclient.filter.FilterException; @@ -180,6 +182,22 @@ public BoundRequestBuilder prepareRequest(RequestBuilder requestBuilder) { @Override public ListenableFuture executeRequest(Request request, AsyncHandler handler) { + if (config.getCookieStore() != null) { + try { + List cookies = config.getCookieStore().get(request.getUri()); + if (!cookies.isEmpty()) { + RequestBuilder requestBuilder = new RequestBuilder(request); + for (Cookie cookie : cookies) + requestBuilder.addOrReplaceCookie(cookie); + + request = requestBuilder.build(); + } + } catch (Exception e) { + handler.onThrowable(e); + return new ListenableFuture.CompletedFailure<>("Failed to set cookies of request", e); + } + } + if (noRequestFilters) { return execute(request, handler); } else { diff --git a/client/src/main/java/org/asynchttpclient/DefaultAsyncHttpClientConfig.java b/client/src/main/java/org/asynchttpclient/DefaultAsyncHttpClientConfig.java index 4fe13015c7..8567079e1d 100644 --- a/client/src/main/java/org/asynchttpclient/DefaultAsyncHttpClientConfig.java +++ b/client/src/main/java/org/asynchttpclient/DefaultAsyncHttpClientConfig.java @@ -26,6 +26,8 @@ import org.asynchttpclient.channel.ChannelPool; import org.asynchttpclient.channel.DefaultKeepAliveStrategy; import org.asynchttpclient.channel.KeepAliveStrategy; +import org.asynchttpclient.cookie.CookieStore; +import org.asynchttpclient.cookie.ThreadSafeCookieStore; import org.asynchttpclient.filter.IOExceptionFilter; import org.asynchttpclient.filter.RequestFilter; import org.asynchttpclient.filter.ResponseFilter; @@ -110,6 +112,9 @@ public class DefaultAsyncHttpClientConfig implements AsyncHttpClientConfig { private final List responseFilters; private final List ioExceptionFilters; + // cookie store + private final CookieStore cookieStore; + // internals private final String threadPoolName; private final int httpClientCodecMaxInitialLineLength; @@ -186,6 +191,9 @@ private DefaultAsyncHttpClientConfig(// List responseFilters,// List ioExceptionFilters,// + // cookie store + CookieStore cookieStore, + // tuning boolean tcpNoDelay,// boolean soReuseAddress,// @@ -263,6 +271,9 @@ private DefaultAsyncHttpClientConfig(// this.responseFilters = responseFilters; this.ioExceptionFilters = ioExceptionFilters; + // cookie store + this.cookieStore = cookieStore; + // tuning this.tcpNoDelay = tcpNoDelay; this.soReuseAddress = soReuseAddress; @@ -502,6 +513,12 @@ public List getIoExceptionFilters() { return ioExceptionFilters; } + // cookie store + @Override + public CookieStore getCookieStore() { + return cookieStore; + } + // tuning @Override public boolean isTcpNoDelay() { @@ -676,6 +693,9 @@ public static class Builder { private final List responseFilters = new LinkedList<>(); private final List ioExceptionFilters = new LinkedList<>(); + // cookie store + private CookieStore cookieStore = new ThreadSafeCookieStore(); + // tuning private boolean tcpNoDelay = defaultTcpNoDelay(); private boolean soReuseAddress = defaultSoReuseAddress(); @@ -1017,6 +1037,12 @@ public Builder removeIOExceptionFilter(IOExceptionFilter ioExceptionFilter) { return this; } + // cookie store + public Builder setCookieStore(CookieStore cookieStore) { + this.cookieStore = cookieStore; + return this; + } + // tuning public Builder setTcpNoDelay(boolean tcpNoDelay) { this.tcpNoDelay = tcpNoDelay; @@ -1191,6 +1217,7 @@ public DefaultAsyncHttpClientConfig build() { requestFilters.isEmpty() ? Collections.emptyList() : Collections.unmodifiableList(requestFilters), // responseFilters.isEmpty() ? Collections.emptyList() : Collections.unmodifiableList(responseFilters),// ioExceptionFilters.isEmpty() ? Collections.emptyList() : Collections.unmodifiableList(ioExceptionFilters),// + cookieStore, tcpNoDelay, // soReuseAddress, // soLinger, // diff --git a/client/src/main/java/org/asynchttpclient/cookie/CookieStore.java b/client/src/main/java/org/asynchttpclient/cookie/CookieStore.java new file mode 100644 index 0000000000..777a1dae22 --- /dev/null +++ b/client/src/main/java/org/asynchttpclient/cookie/CookieStore.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2017 AsyncHttpClient Project. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ + +package org.asynchttpclient.cookie; + +import io.netty.handler.codec.http.cookie.Cookie; +import org.asynchttpclient.uri.Uri; + +import java.net.CookieManager; +import java.util.List; +import java.util.function.Predicate; + +/** + * This interface represents an abstract store for {@link Cookie} objects. + *

+ *

{@link CookieManager} will call {@code CookieStore.add} to save cookies + * for every incoming HTTP response, and call {@code CookieStore.get} to + * retrieve cookie for every outgoing HTTP request. A CookieStore + * is responsible for removing HttpCookie instances which have expired. + * + * @since 2.1 + */ +public interface CookieStore { + /** + * Adds one {@link Cookie} to the store. This is called for every incoming HTTP response. + * If the given cookie has already expired it will not be added, but existing values will still be removed. + *

+ *

A cookie to store may or may not be associated with an URI. If it + * is not associated with an URI, the cookie's domain and path attribute + * will indicate where it comes from. If it is associated with an URI and + * its domain and path attribute are not specified, given URI will indicate + * where this cookie comes from. + *

+ *

If a cookie corresponding to the given URI already exists, + * then it is replaced with the new one. + * + * @param uri the {@link Uri uri} this cookie associated with. if {@code null}, this cookie will not be associated with an URI + * @param cookie the {@link Cookie cookie} to be added + */ + void add(Uri uri, Cookie cookie); + + /** + * Retrieve cookies associated with given URI, or whose domain matches the given URI. Only cookies that + * have not expired are returned. This is called for every outgoing HTTP request. + * + * @param uri the {@link Uri uri} associated with the cookies to be returned + * @return an immutable list of Cookie, return empty list if no cookies match the given URI + */ + List get(Uri uri); + + /** + * Get all not-expired cookies in cookie store. + * + * @return an immutable list of http cookies; + * return empty list if there's no http cookie in store + */ + List getAll(); + + /** + * Remove a cookie from store. + * + * @param predicate that indicates what cookies to remove + * @return {@code true} if this store contained the specified cookie + * @throws NullPointerException if {@code cookie} is {@code null} + */ + boolean remove(Predicate predicate); + + /** + * Remove all cookies in this cookie store. + * + * @return true if any cookies were purged. + */ + boolean clear(); +} diff --git a/client/src/main/java/org/asynchttpclient/cookie/ThreadSafeCookieStore.java b/client/src/main/java/org/asynchttpclient/cookie/ThreadSafeCookieStore.java new file mode 100644 index 0000000000..03b1a70c3b --- /dev/null +++ b/client/src/main/java/org/asynchttpclient/cookie/ThreadSafeCookieStore.java @@ -0,0 +1,238 @@ +/* + * Copyright (c) 2017 AsyncHttpClient Project. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ + +package org.asynchttpclient.cookie; + +import io.netty.handler.codec.http.cookie.Cookie; +import org.asynchttpclient.uri.Uri; +import org.asynchttpclient.util.Assertions; +import org.asynchttpclient.util.MiscUtils; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +public final class ThreadSafeCookieStore implements CookieStore { + + private Map cookieJar = new ConcurrentHashMap<>(); + + @Override + public void add(Uri uri, Cookie cookie) { + String thisRequestDomain = requestDomain(uri); + String thisRequestPath = requestPath(uri); + + add(thisRequestDomain, thisRequestPath, cookie); + } + + @Override + public List get(Uri uri) { + return get(requestDomain(uri), requestPath(uri), uri.isSecured()); + } + + @Override + public List getAll() { + final boolean[] removeExpired = {false}; + List result = cookieJar + .entrySet() + .stream() + .filter(pair -> { + boolean hasCookieExpired = hasCookieExpired(pair.getValue().cookie, pair.getValue().createdAt); + if (hasCookieExpired && !removeExpired[0]) + removeExpired[0] = true; + return !hasCookieExpired; + }) + .map(pair -> pair.getValue().cookie) + .collect(Collectors.toList()); + + if (removeExpired[0]) + removeExpired(); + + return result; + } + + @Override + public boolean remove(Predicate predicate) { + return cookieJar.entrySet().removeIf(v -> predicate.test(v.getValue().cookie)); + } + + @Override + public boolean clear() { + boolean result = !cookieJar.isEmpty(); + cookieJar.clear(); + return result; + } + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private String requestDomain(Uri requestUri) { + return requestUri.getHost().toLowerCase(); + } + + private String requestPath(Uri requestUri) { + return requestUri.getPath().isEmpty() ? "/" : requestUri.getPath(); + } + + // rfc6265#section-5.2.3 + // Let cookie-domain be the attribute-value without the leading %x2E (".") character. + private AbstractMap.SimpleEntry cookieDomain(String cookieDomain, String requestDomain) { + if (cookieDomain != null) { + String normalizedCookieDomain = cookieDomain.toLowerCase(); + return new AbstractMap.SimpleEntry<>( + (!cookieDomain.isEmpty() && cookieDomain.charAt(0) == '.') ? + normalizedCookieDomain.substring(1) : + normalizedCookieDomain, false); + } else + return new AbstractMap.SimpleEntry<>(requestDomain, true); + } + + // rfc6265#section-5.2.4 + private String cookiePath(String rawCookiePath, String requestPath) { + if (MiscUtils.isNonEmpty(rawCookiePath) && rawCookiePath.charAt(0) == '/') { + return rawCookiePath; + } else { + // rfc6265#section-5.1.4 + int indexOfLastSlash = requestPath.lastIndexOf('/'); + if (!requestPath.isEmpty() && requestPath.charAt(0) == '/' && indexOfLastSlash > 0) + return requestPath.substring(0, indexOfLastSlash); + else + return "/"; + } + } + + private boolean hasCookieExpired(Cookie cookie, long whenCreated) { + // if not specify max-age, this cookie should be discarded when user agent is to be closed, but it is not expired. + if (cookie.maxAge() == Cookie.UNDEFINED_MAX_AGE) + return false; + + if (cookie.maxAge() <= 0) + return true; + + if (whenCreated > 0) { + long deltaSecond = (System.currentTimeMillis() - whenCreated) / 1000; + return deltaSecond > cookie.maxAge(); + } else + return false; + } + + // rfc6265#section-5.1.3 + // check "The string is a host name (i.e., not an IP address)" ignored + private boolean domainsMatch(String cookieDomain, String requestDomain, boolean hostOnly) { + return (hostOnly && Objects.equals(requestDomain, cookieDomain)) || + (Objects.equals(requestDomain, cookieDomain) || requestDomain.endsWith("." + cookieDomain)); + } + + // rfc6265#section-5.1.4 + private boolean pathsMatch(String cookiePath, String requestPath) { + return Objects.equals(cookiePath, requestPath) || + (requestPath.startsWith(cookiePath) && (cookiePath.charAt(cookiePath.length() - 1) == '/' || requestPath.charAt(cookiePath.length()) == '/')); + } + + private static class CookieKey implements Comparable { + final String name; + final String domain; + final String path; + + CookieKey(String name, String domain, String path) { + this.name = name; + this.domain = domain; + this.path = path; + } + + @Override + public int compareTo(CookieKey o) { + Assertions.assertNotNull(o, "Parameter can't be null"); + int result; + if ((result = this.name.compareTo(o.name)) == 0) + if ((result = this.domain.compareTo(o.domain)) == 0) + result = this.path.compareTo(o.path); + + return result; + } + + @Override + public boolean equals(Object obj) { + return obj instanceof CookieKey && this.compareTo((CookieKey) obj) == 0; + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + name.hashCode(); + result = 31 * result + domain.hashCode(); + result = 31 * result + path.hashCode(); + return result; + } + + @Override + public String toString() { + return String.format("%s: %s; %s", name, domain, path); + } + } + + private static class StoredCookie { + final Cookie cookie; + final boolean hostOnly; + final boolean persistent; + final long createdAt = System.currentTimeMillis(); + + StoredCookie(Cookie cookie, boolean hostOnly, boolean persistent) { + this.cookie = cookie; + this.hostOnly = hostOnly; + this.persistent = persistent; + } + + @Override + public String toString() { + return String.format("%s; hostOnly %s; persistent %s", cookie.toString(), hostOnly, persistent); + } + } + + private void add(String requestDomain, String requestPath, Cookie cookie) { + + AbstractMap.SimpleEntry pair = cookieDomain(cookie.domain(), requestDomain); + String keyDomain = pair.getKey(); + boolean hostOnly = pair.getValue(); + String keyPath = cookiePath(cookie.path(), requestPath); + CookieKey key = new CookieKey(cookie.name().toLowerCase(), keyDomain, keyPath); + + if (hasCookieExpired(cookie, 0)) + cookieJar.remove(key); + else + cookieJar.put(key, new StoredCookie(cookie, hostOnly, cookie.maxAge() != Cookie.UNDEFINED_MAX_AGE)); + } + + private List get(String domain, String path, boolean secure) { + + final boolean[] removeExpired = {false}; + + List result = cookieJar.entrySet().stream().filter(pair -> { + CookieKey key = pair.getKey(); + StoredCookie storedCookie = pair.getValue(); + boolean hasCookieExpired = hasCookieExpired(storedCookie.cookie, storedCookie.createdAt); + if (hasCookieExpired && !removeExpired[0]) + removeExpired[0] = true; + return !hasCookieExpired && domainsMatch(key.domain, domain, storedCookie.hostOnly) && pathsMatch(key.path, path) && (secure || !storedCookie.cookie.isSecure()); + }).map(v -> v.getValue().cookie).collect(Collectors.toList()); + + if (removeExpired[0]) + removeExpired(); + + return result; + } + + private void removeExpired() { + cookieJar.entrySet().removeIf(v -> hasCookieExpired(v.getValue().cookie, v.getValue().createdAt)); + } +} diff --git a/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Interceptors.java b/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Interceptors.java index ce144d38b4..cf212b4cc2 100644 --- a/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Interceptors.java +++ b/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Interceptors.java @@ -13,6 +13,7 @@ */ package org.asynchttpclient.netty.handler.intercept; +import static io.netty.handler.codec.http.HttpHeaderNames.SET_COOKIE; import static org.asynchttpclient.util.HttpConstants.ResponseStatusCodes.*; import io.netty.channel.Channel; import io.netty.handler.codec.http.HttpHeaders; @@ -20,11 +21,14 @@ import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponse; +import io.netty.handler.codec.http.cookie.ClientCookieDecoder; +import io.netty.handler.codec.http.cookie.Cookie; import org.asynchttpclient.AsyncHandler; import org.asynchttpclient.AsyncHttpClientConfig; import org.asynchttpclient.HttpResponseStatus; import org.asynchttpclient.Realm; import org.asynchttpclient.Request; +import org.asynchttpclient.cookie.CookieStore; import org.asynchttpclient.netty.NettyResponseFuture; import org.asynchttpclient.netty.channel.ChannelManager; import org.asynchttpclient.netty.request.NettyRequestSender; @@ -69,6 +73,15 @@ public boolean exitAfterIntercept(// Request request = future.getCurrentRequest(); Realm realm = request.getRealm() != null ? request.getRealm() : config.getRealm(); + // This MUST BE called before Redirect30xInterceptor because latter assumes cookie store is already updated + CookieStore cookieStore = config.getCookieStore(); + if (cookieStore != null) { + for (String cookieStr : responseHeaders.getAll(SET_COOKIE)) { + Cookie c = ClientCookieDecoder.STRICT.decode(cookieStr); + cookieStore.add(future.getCurrentRequest().getUri(), c); + } + } + if (hasResponseFilters && responseFiltersInterceptor.exitAfterProcessingFilters(channel, future, handler, status, responseHeaders)) { return true; } diff --git a/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Redirect30xInterceptor.java b/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Redirect30xInterceptor.java index 90d9d7612c..39bc3a1e27 100644 --- a/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Redirect30xInterceptor.java +++ b/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Redirect30xInterceptor.java @@ -27,6 +27,7 @@ import io.netty.handler.codec.http.cookie.Cookie; import java.util.HashSet; +import java.util.List; import java.util.Set; import org.asynchttpclient.AsyncHttpClientConfig; @@ -34,6 +35,7 @@ import org.asynchttpclient.Realm.AuthScheme; import org.asynchttpclient.Request; import org.asynchttpclient.RequestBuilder; +import org.asynchttpclient.cookie.CookieStore; import org.asynchttpclient.handler.MaxRedirectException; import org.asynchttpclient.netty.NettyResponseFuture; import org.asynchttpclient.netty.channel.ChannelManager; @@ -91,7 +93,6 @@ public boolean exitAfterHandlingRedirect(// boolean keepBody = statusCode == TEMPORARY_REDIRECT_307 || statusCode == PERMANENT_REDIRECT_308 || (statusCode == FOUND_302 && config.isStrict302Handling()); final RequestBuilder requestBuilder = new RequestBuilder(switchToGet ? GET : originalMethod)// - .setCookies(request.getCookies())// .setChannelPoolPartitioning(request.getChannelPoolPartitioning())// .setFollowRedirect(true)// .setLocalAddress(request.getLocalAddress())// @@ -127,10 +128,13 @@ else if (request.getBodyGenerator() != null) LOGGER.debug("Redirecting to {}", newUri); - for (String cookieStr : responseHeaders.getAll(SET_COOKIE)) { - Cookie c = ClientCookieDecoder.STRICT.decode(cookieStr); - if (c != null) - requestBuilder.addOrReplaceCookie(c); + CookieStore cookieStore = config.getCookieStore(); + if (cookieStore != null) { + // Update request's cookies assuming that cookie store is already updated by Interceptors + List cookies = cookieStore.get(newUri); + if (!cookies.isEmpty()) + for (Cookie cookie : cookies) + requestBuilder.addOrReplaceCookie(cookie); } boolean sameBase = isSameBase(request.getUri(), newUri); diff --git a/client/src/test/java/org/asynchttpclient/CookieStoreTest.java b/client/src/test/java/org/asynchttpclient/CookieStoreTest.java new file mode 100644 index 0000000000..3ccfcc443a --- /dev/null +++ b/client/src/test/java/org/asynchttpclient/CookieStoreTest.java @@ -0,0 +1,340 @@ +/* + * Copyright (c) 2017 AsyncHttpClient Project. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ + +package org.asynchttpclient; + +import io.netty.handler.codec.http.cookie.ClientCookieDecoder; +import io.netty.handler.codec.http.cookie.ClientCookieEncoder; +import io.netty.handler.codec.http.cookie.Cookie; +import org.asynchttpclient.cookie.CookieStore; +import org.asynchttpclient.cookie.ThreadSafeCookieStore; +import org.asynchttpclient.uri.Uri; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import java.util.List; + +import static org.testng.Assert.assertTrue; + +public class CookieStoreTest { + + private final Logger logger = LoggerFactory.getLogger(getClass()); + + @BeforeClass(alwaysRun = true) + public void setUpGlobal() { + logger.info("Local HTTP server started successfully"); + System.out.println("--Start"); + } + + @AfterClass(alwaysRun = true) + public void tearDownGlobal() { + System.out.println("--Stop"); + } + + @Test(groups = "standalone") + public void runAllSequentiallyBecauseNotThreadSafe() { + addCookieWithEmptyPath(); + dontReturnCookieForAnotherDomain(); + returnCookieWhenItWasSetOnSamePath(); + returnCookieWhenItWasSetOnParentPath(); + dontReturnCookieWhenDomainMatchesButPathIsDifferent(); + dontReturnCookieWhenDomainMatchesButPathIsParent(); + returnCookieWhenDomainMatchesAndPathIsChild(); + returnCookieWhenItWasSetOnSubdomain(); + replaceCookieWhenSetOnSameDomainAndPath(); + dontReplaceCookiesWhenTheyHaveDifferentName(); + expireCookieWhenSetWithDateInThePast(); + cookieWithSameNameMustCoexistIfSetOnDifferentDomains(); + handleMissingDomainAsRequestHost(); + handleMissingPathAsSlash(); + returnTheCookieWheniTSissuedFromRequestWithSubpath(); + handleMissingPathAsRequestPathWhenFromRootDir(); + handleMissingPathAsRequestPathWhenPathIsNotEmpty(); + handleDomainInCaseInsensitiveManner(); + handleCookieNameInCaseInsensitiveManner(); + handleCookiePathInCaseSensitiveManner(); + ignoreQueryParametersInUri(); + shouldServerOnSubdomainWhenDomainMatches(); + replaceCookieWhenSetOnSamePathBySameUri(); + handleMultipleCookieOfSameNameOnDifferentPaths(); + handleTrailingSlashesInPaths(); + returnMultipleCookiesEvenIfTheyHaveSameName(); + shouldServeCookiesBasedOnTheUriScheme(); + shouldAlsoServeNonSecureCookiesBasedOnTheUriScheme(); + shouldNotServeSecureCookiesForDefaultRetrievedHttpUriScheme(); + shouldServeSecureCookiesForSpecificallyRetrievedHttpUriScheme(); + } + + private void addCookieWithEmptyPath() { + CookieStore store = new ThreadSafeCookieStore(); + Uri uri = Uri.create("http://www.foo.com"); + store.add(uri, ClientCookieDecoder.LAX.decode("ALPHA=VALUE1; path=")); + assertTrue(store.get(uri).size() > 0); + } + + private void dontReturnCookieForAnotherDomain() { + CookieStore store = new ThreadSafeCookieStore(); + store.add(Uri.create("http://www.foo.com"), ClientCookieDecoder.LAX.decode("ALPHA=VALUE1; path=")); + assertTrue(store.get(Uri.create("http://www.bar.com")).isEmpty()); + } + + private void returnCookieWhenItWasSetOnSamePath() { + CookieStore store = new ThreadSafeCookieStore(); + store.add(Uri.create("http://www.foo.com"), ClientCookieDecoder.LAX.decode("ALPHA=VALUE1; path=/bar/")); + assertTrue(store.get(Uri.create("http://www.foo.com/bar/")).size() == 1); + } + + private void returnCookieWhenItWasSetOnParentPath() { + CookieStore store = new ThreadSafeCookieStore(); + store.add(Uri.create("http://www.foo.com"), ClientCookieDecoder.LAX.decode("ALPHA=VALUE1; Domain=www.foo.com; path=/bar")); + assertTrue(store.get(Uri.create("http://www.foo.com/bar/baz")).size() == 1); + } + + private void dontReturnCookieWhenDomainMatchesButPathIsDifferent() { + CookieStore store = new ThreadSafeCookieStore(); + store.add(Uri.create("http://www.foo.com/bar"), ClientCookieDecoder.LAX.decode("ALPHA=VALUE1; Domain=www.foo.com; path=/bar")); + assertTrue(store.get(Uri.create("http://www.foo.com/baz")).isEmpty()); + } + + private void dontReturnCookieWhenDomainMatchesButPathIsParent() { + CookieStore store = new ThreadSafeCookieStore(); + store.add(Uri.create("http://www.foo.com/bar"), ClientCookieDecoder.LAX.decode("ALPHA=VALUE1; Domain=www.foo.com; path=/bar")); + assertTrue(store.get(Uri.create("http://www.foo.com")).isEmpty()); + } + + private void returnCookieWhenDomainMatchesAndPathIsChild() { + CookieStore store = new ThreadSafeCookieStore(); + store.add(Uri.create("http://www.foo.com/bar"), ClientCookieDecoder.LAX.decode("ALPHA=VALUE1; Domain=www.foo.com; path=/bar")); + assertTrue(store.get(Uri.create("http://www.foo.com/bar/baz")).size() == 1); + } + + private void returnCookieWhenItWasSetOnSubdomain() { + CookieStore store = new ThreadSafeCookieStore(); + store.add(Uri.create("http://www.foo.com"), ClientCookieDecoder.LAX.decode("ALPHA=VALUE1; Domain=.foo.com")); + assertTrue(store.get(Uri.create("http://bar.foo.com")).size() == 1); + } + + private void replaceCookieWhenSetOnSameDomainAndPath() { + CookieStore store = new ThreadSafeCookieStore(); + Uri uri = Uri.create("http://www.foo.com/bar/baz"); + store.add(uri, ClientCookieDecoder.LAX.decode("ALPHA=VALUE1; Domain=www.foo.com; path=/bar")); + store.add(uri, ClientCookieDecoder.LAX.decode("ALPHA=VALUE2; Domain=www.foo.com; path=/bar")); + assertTrue(store.getAll().size() == 1); + assertTrue(store.get(uri).get(0).value().equals("VALUE2")); + } + + private void dontReplaceCookiesWhenTheyHaveDifferentName() { + CookieStore store = new ThreadSafeCookieStore(); + Uri uri = Uri.create("http://www.foo.com/bar/baz"); + store.add(uri, ClientCookieDecoder.LAX.decode("BETA=VALUE1; Domain=www.foo.com; path=/bar")); + store.add(uri, ClientCookieDecoder.LAX.decode("ALPHA=VALUE2; Domain=www.foo.com; path=/bar")); + assertTrue(store.get(uri).size() == 2); + } + + private void expireCookieWhenSetWithDateInThePast() { + CookieStore store = new ThreadSafeCookieStore(); + Uri uri = Uri.create("http://www.foo.com/bar"); + store.add(uri, ClientCookieDecoder.LAX.decode("ALPHA=VALUE1; Domain=www.foo.com; path=/bar")); + store.add(uri, ClientCookieDecoder.LAX.decode("ALPHA=EXPIRED; Domain=www.foo.com; Path=/bar; Expires=Sun, 06 Nov 1994 08:49:37 GMT")); + assertTrue(store.getAll().isEmpty()); + } + + private void cookieWithSameNameMustCoexistIfSetOnDifferentDomains() { + CookieStore store = new ThreadSafeCookieStore(); + Uri uri1 = Uri.create("http://www.foo.com"); + store.add(uri1, ClientCookieDecoder.LAX.decode("ALPHA=VALUE1; Domain=www.foo.com")); + Uri uri2 = Uri.create("http://www.bar.com"); + store.add(uri2, ClientCookieDecoder.LAX.decode("ALPHA=VALUE2; Domain=www.bar.com")); + + assertTrue(store.get(uri1).size() == 1); + assertTrue(store.get(uri1).get(0).value().equals("VALUE1")); + + assertTrue(store.get(uri2).size() == 1); + assertTrue(store.get(uri2).get(0).value().equals("VALUE2")); + } + + private void handleMissingDomainAsRequestHost() { + CookieStore store = new ThreadSafeCookieStore(); + Uri uri = Uri.create("http://www.foo.com"); + store.add(uri, ClientCookieDecoder.LAX.decode("ALPHA=VALUE1; Path=/")); + assertTrue(store.get(uri).size() == 1); + } + + private void handleMissingPathAsSlash() { + CookieStore store = new ThreadSafeCookieStore(); + Uri uri = Uri.create("http://www.foo.com"); + store.add(uri, ClientCookieDecoder.LAX.decode("tooe_token=0b1d81dd02d207491a6e9b0a2af9470da9eb1dad")); + assertTrue(store.get(uri).size() == 1); + } + + private void returnTheCookieWheniTSissuedFromRequestWithSubpath() { + CookieStore store = new ThreadSafeCookieStore(); + store.add(Uri.create("http://www.foo.com/bar"), ClientCookieDecoder.LAX.decode("ALPHA=VALUE; path=/")); + assertTrue(store.get(Uri.create("http://www.foo.com")).size() == 1); + } + + private void handleMissingPathAsRequestPathWhenFromRootDir() { + CookieStore store = new ThreadSafeCookieStore(); + Uri uri = Uri.create("http://www.foo.com"); + store.add(uri, ClientCookieDecoder.LAX.decode("ALPHA=VALUE1")); + assertTrue(store.get(uri).size() == 1); + } + + private void handleMissingPathAsRequestPathWhenPathIsNotEmpty() { + CookieStore store = new ThreadSafeCookieStore(); + store.add(Uri.create("http://www.foo.com/bar"), ClientCookieDecoder.LAX.decode("ALPHA=VALUE1; Domain=www.foo.com; path=/bar")); + assertTrue(store.get(Uri.create("http://www.foo.com/baz")).isEmpty()); + } + + // RFC 2965 sec. 3.3.3 + private void handleDomainInCaseInsensitiveManner() { + CookieStore store = new ThreadSafeCookieStore(); + store.add(Uri.create("http://www.foo.com/bar"), ClientCookieDecoder.LAX.decode("ALPHA=VALUE1")); + assertTrue(store.get(Uri.create("http://www.FoO.com/bar")).size() == 1); + } + + // RFC 2965 sec. 3.3.3 + private void handleCookieNameInCaseInsensitiveManner() { + CookieStore store = new ThreadSafeCookieStore(); + Uri uri = Uri.create("http://www.foo.com/bar/baz"); + store.add(uri, ClientCookieDecoder.LAX.decode("ALPHA=VALUE1; Domain=www.foo.com; path=/bar")); + store.add(uri, ClientCookieDecoder.LAX.decode("alpha=VALUE2; Domain=www.foo.com; path=/bar")); + assertTrue(store.getAll().size() == 1); + assertTrue(store.get(uri).get(0).value().equals("VALUE2")); + } + + // RFC 2965 sec. 3.3.3 + private void handleCookiePathInCaseSensitiveManner() { + CookieStore store = new ThreadSafeCookieStore(); + store.add(Uri.create("http://www.foo.com/foo/bar"), ClientCookieDecoder.LAX.decode("ALPHA=VALUE1")); + assertTrue(store.get(Uri.create("http://www.FoO.com/Foo/bAr")).isEmpty()); + } + + private void ignoreQueryParametersInUri() { + CookieStore store = new ThreadSafeCookieStore(); + store.add(Uri.create("http://www.foo.com/bar?query1"), ClientCookieDecoder.LAX.decode("ALPHA=VALUE1; Domain=www.foo.com; path=/")); + assertTrue(store.get(Uri.create("http://www.foo.com/bar?query2")).size() == 1); + } + + // RFC 6265, 5.1.3. Domain Matching + private void shouldServerOnSubdomainWhenDomainMatches() { + CookieStore store = new ThreadSafeCookieStore(); + store.add(Uri.create("https://x.foo.org/"), ClientCookieDecoder.LAX.decode("cookie1=VALUE1; Path=/; Domain=foo.org;")); + assertTrue(store.get(Uri.create("https://y.x.foo.org/")).size() == 1); + } + + // NOTE: Similar to replaceCookieWhenSetOnSameDomainAndPath() + private void replaceCookieWhenSetOnSamePathBySameUri() { + CookieStore store = new ThreadSafeCookieStore(); + Uri uri = Uri.create("https://foo.org/"); + store.add(uri, ClientCookieDecoder.LAX.decode("cookie1=VALUE1; Path=/")); + store.add(uri, ClientCookieDecoder.LAX.decode("cookie1=VALUE2; Path=/")); + store.add(uri, ClientCookieDecoder.LAX.decode("cookie1=VALUE3; Path=/")); + assertTrue(store.getAll().size() == 1); + assertTrue(store.get(uri).get(0).value().equals("VALUE3")); + } + + private void handleMultipleCookieOfSameNameOnDifferentPaths() { + CookieStore store = new ThreadSafeCookieStore(); + store.add(Uri.create("http://www.foo.com"), ClientCookieDecoder.LAX.decode("cookie=VALUE0; path=/")); + store.add(Uri.create("http://www.foo.com/foo/bar"), ClientCookieDecoder.LAX.decode("cookie=VALUE1; path=/foo/bar/")); + store.add(Uri.create("http://www.foo.com/foo/baz"), ClientCookieDecoder.LAX.decode("cookie=VALUE2; path=/foo/baz/")); + + Uri uri1 = Uri.create("http://www.foo.com/foo/bar/"); + List cookies1 = store.get(uri1); + assertTrue(cookies1.size() == 2); + assertTrue(cookies1.stream().filter(c -> c.value().equals("VALUE0") || c.value().equals("VALUE1")).count() == 2); + + Uri uri2 = Uri.create("http://www.foo.com/foo/baz/"); + List cookies2 = store.get(uri2); + assertTrue(cookies2.size() == 2); + assertTrue(cookies2.stream().filter(c -> c.value().equals("VALUE0") || c.value().equals("VALUE2")).count() == 2); + } + + private void handleTrailingSlashesInPaths() { + CookieStore store = new ThreadSafeCookieStore(); + store.add( + Uri.create("https://vagrant.moolb.com/app/consumer/j_spring_cas_security_check?ticket=ST-5-Q7gzqPpvG3N3Bb02bm3q-llinder-vagrantmgr.moolb.com"), + ClientCookieDecoder.LAX.decode("JSESSIONID=211D17F016132BCBD31D9ABB31D90960; Path=/app/consumer/; HttpOnly")); + assertTrue(store.getAll().size() == 1); + assertTrue(store.get(Uri.create("https://vagrant.moolb.com/app/consumer/")).get(0).value().equals("211D17F016132BCBD31D9ABB31D90960")); + } + + private void returnMultipleCookiesEvenIfTheyHaveSameName() { + CookieStore store = new ThreadSafeCookieStore(); + store.add(Uri.create("http://foo.com"), ClientCookieDecoder.LAX.decode("JSESSIONID=FOO; Domain=.foo.com")); + store.add(Uri.create("http://sub.foo.com"), ClientCookieDecoder.LAX.decode("JSESSIONID=BAR; Domain=sub.foo.com")); + + Uri uri1 = Uri.create("http://sub.foo.com"); + List cookies1 = store.get(uri1); + assertTrue(cookies1.size() == 2); + assertTrue(cookies1.stream().filter(c -> c.value().equals("FOO") || c.value().equals("BAR")).count() == 2); + + String result = ClientCookieEncoder.LAX.encode(cookies1.get(0), cookies1.get(1)); + assertTrue(result.equals("JSESSIONID=FOO; JSESSIONID=BAR")); + } + + // rfc6265#section-1 Cookies for a given host are shared across all the ports on that host + private void shouldServeCookiesBasedOnTheUriScheme() { + CookieStore store = new ThreadSafeCookieStore(); + store.add(Uri.create("https://foo.org/moodle/"), ClientCookieDecoder.LAX.decode("cookie1=VALUE1; Path=/")); + store.add(Uri.create("https://foo.org:443/moodle/login"), ClientCookieDecoder.LAX.decode("cookie1=VALUE2; Path=/")); + store.add(Uri.create("https://foo.org:443/moodle/login"), ClientCookieDecoder.LAX.decode("cookie1=VALUE3; Path=/; Secure")); + + Uri uri = Uri.create("https://foo.org/moodle/login"); + assertTrue(store.getAll().size() == 1); + assertTrue(store.get(uri).get(0).value().equals("VALUE3")); + assertTrue(store.get(uri).get(0).isSecure()); + } + + // rfc6265#section-1 Cookies for a given host are shared across all the ports on that host + private void shouldAlsoServeNonSecureCookiesBasedOnTheUriScheme() { + CookieStore store = new ThreadSafeCookieStore(); + store.add(Uri.create("https://foo.org/moodle/"), ClientCookieDecoder.LAX.decode("cookie1=VALUE1; Path=/")); + store.add(Uri.create("https://foo.org:443/moodle/login"), ClientCookieDecoder.LAX.decode("cookie1=VALUE2; Path=/")); + store.add(Uri.create("https://foo.org:443/moodle/login"), ClientCookieDecoder.LAX.decode("cookie1=VALUE3; Path=/; HttpOnly")); + + Uri uri = Uri.create("https://foo.org/moodle/login"); + assertTrue(store.getAll().size() == 1); + assertTrue(store.get(uri).get(0).value().equals("VALUE3")); + assertTrue(!store.get(uri).get(0).isSecure()); + } + + // rfc6265#section-1 Cookies for a given host are shared across all the ports on that host + private void shouldNotServeSecureCookiesForDefaultRetrievedHttpUriScheme() { + CookieStore store = new ThreadSafeCookieStore(); + store.add(Uri.create("https://foo.org/moodle/"), ClientCookieDecoder.LAX.decode("cookie1=VALUE1; Path=/")); + store.add(Uri.create("https://foo.org:443/moodle/login"), ClientCookieDecoder.LAX.decode("cookie1=VALUE2; Path=/")); + store.add(Uri.create("https://foo.org:443/moodle/login"), ClientCookieDecoder.LAX.decode("cookie1=VALUE3; Path=/; Secure")); + + Uri uri = Uri.create("http://foo.org/moodle/login"); + assertTrue(store.get(uri).isEmpty()); + } + + // rfc6265#section-1 Cookies for a given host are shared across all the ports on that host + private void shouldServeSecureCookiesForSpecificallyRetrievedHttpUriScheme() { + CookieStore store = new ThreadSafeCookieStore(); + store.add(Uri.create("https://foo.org/moodle/"), ClientCookieDecoder.LAX.decode("cookie1=VALUE1; Path=/")); + store.add(Uri.create("https://foo.org:443/moodle/login"), ClientCookieDecoder.LAX.decode("cookie1=VALUE2; Path=/")); + store.add(Uri.create("https://foo.org:443/moodle/login"), ClientCookieDecoder.LAX.decode("cookie1=VALUE3; Path=/; Secure")); + + Uri uri = Uri.create("https://foo.org/moodle/login"); + assertTrue(store.get(uri).size() == 1); + assertTrue(store.get(uri).get(0).value().equals("VALUE3")); + assertTrue(store.get(uri).get(0).isSecure()); + } +}