From 4513365e72cc5271f34bd53c8e2d1fa9672567d8 Mon Sep 17 00:00:00 2001 From: Aayush Atharva Date: Wed, 23 Sep 2026 20:32:01 +0000 Subject: [PATCH 1/2] Keep a caller's cookie when the store refuses Set-Cookie --- .../intercept/Redirect30xInterceptor.java | 19 ++++++++++++++++--- .../intercept/RedirectCookieRotationTest.java | 15 +++++++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) 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 39360acda..2225d2687 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 @@ -212,7 +212,7 @@ public boolean exitAfterHandlingRedirect(Channel channel, NettyResponseFuture requestBuilder.resetCookies(); } else { requestBuilder.setCookies(cookieStore == null - ? request.getCookies() : callersOwnCookies(request, response, cookieStore)); + ? request.getCookies() : callersOwnCookies(request, response, cookieStore, newUri)); } requestBuilder.setMethod(switchToGet ? GET : originalMethod) @@ -465,15 +465,28 @@ private enum BodyRepresentation { * and those the store still holds with the same value. A caller's cookie sharing only a name with a stored * one stays the caller's. */ - private List callersOwnCookies(Request request, HttpResponse response, CookieStore cookieStore) { + private List callersOwnCookies(Request request, HttpResponse response, CookieStore cookieStore, Uri newUri) { List cookies = request.getCookies(); if (cookies.isEmpty()) { return cookies; } + // Only what the store took counts: a Set-Cookie it refused, or one for another path, leaves the + // caller's cookie of that name in place. Set setByResponse = new HashSet<>(); + List next = null; for (String header : response.headers().getAll(SET_COOKIE)) { Cookie cookie = cookieDecoder.decode(header); - if (cookie != null) { + if (cookie == null) { + continue; + } + if (cookie.maxAge() != Cookie.UNDEFINED_MAX_AGE && cookie.maxAge() <= 0) { + setByResponse.add(cookie.name()); + continue; + } + if (next == null) { + next = cookieStore.get(newUri); + } + if (holdsSameValue(next, cookie)) { setByResponse.add(cookie.name()); } } diff --git a/client/src/test/java/org/asynchttpclient/netty/handler/intercept/RedirectCookieRotationTest.java b/client/src/test/java/org/asynchttpclient/netty/handler/intercept/RedirectCookieRotationTest.java index 097611568..41e1054bf 100644 --- a/client/src/test/java/org/asynchttpclient/netty/handler/intercept/RedirectCookieRotationTest.java +++ b/client/src/test/java/org/asynchttpclient/netty/handler/intercept/RedirectCookieRotationTest.java @@ -83,6 +83,12 @@ public void handle(String target, Request baseRequest, HttpServletRequest reques case "/bounce-307": redirect(response, 307, null, "/home"); break; + case "/reject-domain": + redirect(response, HttpServletResponse.SC_FOUND, "SID=evil; Domain=example.org; Path=/", "/home"); + break; + case "/other-path": + redirect(response, HttpServletResponse.SC_FOUND, "SID=other; Path=/elsewhere", "/home"); + break; case "/see-other": redirect(response, HttpServletResponse.SC_SEE_OTHER, null, "/home"); break; @@ -159,6 +165,15 @@ void theCallersCookieStillBeatsAStoredOneOfTheSameName() throws Exception { assertEquals("SID=mine", withCallerCookie("SID", "mine", client -> client.prepareGet(url("/bounce")))); } + /** A Set-Cookie the store refused, or filed for another path, does not replace the caller's cookie. */ + @Test + void aSetCookieThatDoesNotReachTheNextHopLeavesTheCallersCookie() throws Exception { + assertEquals("SID=mine", withCallerCookie("SID", "mine", client -> client.prepareGet(url("/reject-domain"))), + "refused: Domain does not match"); + assertEquals("SID=mine", withCallerCookie("SID", "mine", client -> client.prepareGet(url("/other-path"))), + "stored for /elsewhere, not sent to /home"); + } + // Without a cookie store every cookie on the request is the caller's own. @Test From 19cbd4c34c5eb3d69e9c2aa0a284a1d8666fc773 Mon Sep 17 00:00:00 2001 From: Aayush Atharva Date: Wed, 23 Sep 2026 20:42:32 +0000 Subject: [PATCH 2/2] Carry the cookies a 401 or 407 challenge set --- .../handler/intercept/CallerCookies.java | 102 ++++++++++++++++++ .../netty/handler/intercept/Interceptors.java | 4 +- .../ProxyUnauthorized407Interceptor.java | 19 +++- .../intercept/Redirect30xInterceptor.java | 54 +--------- .../intercept/Unauthorized401Interceptor.java | 26 ++++- .../intercept/AuthRetryCookieTest.java | 94 ++++++++++++++++ 6 files changed, 240 insertions(+), 59 deletions(-) create mode 100644 client/src/main/java/org/asynchttpclient/netty/handler/intercept/CallerCookies.java create mode 100644 client/src/test/java/org/asynchttpclient/netty/handler/intercept/AuthRetryCookieTest.java diff --git a/client/src/main/java/org/asynchttpclient/netty/handler/intercept/CallerCookies.java b/client/src/main/java/org/asynchttpclient/netty/handler/intercept/CallerCookies.java new file mode 100644 index 000000000..9fbef2646 --- /dev/null +++ b/client/src/main/java/org/asynchttpclient/netty/handler/intercept/CallerCookies.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2026 AsyncHttpClient Project. All rights reserved. + * + * Licensed 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. + */ +package org.asynchttpclient.netty.handler.intercept; + +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.Request; +import org.asynchttpclient.RequestBuilder; +import org.asynchttpclient.cookie.CookieStore; +import org.asynchttpclient.uri.Uri; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static io.netty.handler.codec.http.HttpHeaderNames.SET_COOKIE; + +/** + * Tells the cookies a caller put on a request from the ones the cookie store added, for the request a redirect + * or an authentication retry builds from it. The store's may be older than what the response just set. + */ +final class CallerCookies { + + private CallerCookies() { + } + + /** + * The request's cookies minus the ones the store put there: those this response set, rotated or deleted, + * and those the store still holds with the same value. A caller's cookie sharing only a name with a stored + * one stays the caller's. + */ + static List of(Request request, HttpResponse response, CookieStore cookieStore, Uri next, + ClientCookieDecoder cookieDecoder) { + List cookies = request.getCookies(); + if (cookies.isEmpty()) { + return cookies; + } + // Only what the store took counts: a Set-Cookie it refused, or one for another path, leaves the + // caller's cookie of that name in place. + Set setByResponse = new HashSet<>(); + List nextCookies = null; + for (String header : response.headers().getAll(SET_COOKIE)) { + Cookie cookie = cookieDecoder.decode(header); + if (cookie == null) { + continue; + } + if (cookie.maxAge() != Cookie.UNDEFINED_MAX_AGE && cookie.maxAge() <= 0) { + setByResponse.add(cookie.name()); + continue; + } + if (nextCookies == null) { + nextCookies = cookieStore.get(next); + } + if (holdsSameValue(nextCookies, cookie)) { + setByResponse.add(cookie.name()); + } + } + List stored = cookieStore.get(request.getUri()); + List callers = new ArrayList<>(cookies.size()); + for (Cookie cookie : cookies) { + if (!setByResponse.contains(cookie.name()) && !holdsSameValue(stored, cookie)) { + callers.add(cookie); + } + } + return callers; + } + + /** + * For a retry of the same request: the caller's cookies, then the store's current ones. + */ + static void refresh(RequestBuilder retry, Request request, HttpResponse response, CookieStore cookieStore, + ClientCookieDecoder cookieDecoder) { + retry.setCookies(of(request, response, cookieStore, request.getUri(), cookieDecoder)); + for (Cookie cookie : cookieStore.get(request.getUri())) { + retry.addCookieIfUnset(cookie); + } + } + + private static boolean holdsSameValue(List stored, Cookie cookie) { + for (Cookie candidate : stored) { + if (candidate.name().equals(cookie.name()) && candidate.value().equals(cookie.value())) { + return true; + } + } + return false; + } +} 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 58a8f095d..884e61c6f 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 @@ -76,8 +76,8 @@ public Interceptors(AsyncHttpClientConfig config, this.config = config; this.requestSender = requestSender; nonceCounter = new NonceCounter(); - unauthorized401Interceptor = new Unauthorized401Interceptor(channelManager, requestSender, nonceCounter); - proxyUnauthorized407Interceptor = new ProxyUnauthorized407Interceptor(channelManager, requestSender, nonceCounter); + unauthorized401Interceptor = new Unauthorized401Interceptor(config, channelManager, requestSender, nonceCounter); + proxyUnauthorized407Interceptor = new ProxyUnauthorized407Interceptor(config, channelManager, requestSender, nonceCounter); continue100Interceptor = new Continue100Interceptor(requestSender); redirect30xInterceptor = new Redirect30xInterceptor(channelManager, config, requestSender); connectSuccessInterceptor = new ConnectSuccessInterceptor(channelManager, requestSender); diff --git a/client/src/main/java/org/asynchttpclient/netty/handler/intercept/ProxyUnauthorized407Interceptor.java b/client/src/main/java/org/asynchttpclient/netty/handler/intercept/ProxyUnauthorized407Interceptor.java index 801792aa7..77bf1e385 100644 --- a/client/src/main/java/org/asynchttpclient/netty/handler/intercept/ProxyUnauthorized407Interceptor.java +++ b/client/src/main/java/org/asynchttpclient/netty/handler/intercept/ProxyUnauthorized407Interceptor.java @@ -47,6 +47,9 @@ import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.List; +import org.asynchttpclient.AsyncHttpClientConfig; +import io.netty.handler.codec.http.cookie.ClientCookieDecoder; +import org.asynchttpclient.cookie.CookieStore; import static io.netty.handler.codec.http.HttpHeaderNames.PROXY_AUTHENTICATE; import static io.netty.handler.codec.http.HttpHeaderNames.PROXY_AUTHORIZATION; @@ -60,11 +63,16 @@ public class ProxyUnauthorized407Interceptor { private static final Logger LOGGER = LoggerFactory.getLogger(ProxyUnauthorized407Interceptor.class); + private final AsyncHttpClientConfig config; + private final ClientCookieDecoder cookieDecoder; private final ChannelManager channelManager; private final NettyRequestSender requestSender; private final NonceCounter nonceCounter; - ProxyUnauthorized407Interceptor(ChannelManager channelManager, NettyRequestSender requestSender, NonceCounter nonceCounter) { + ProxyUnauthorized407Interceptor(AsyncHttpClientConfig config, ChannelManager channelManager, NettyRequestSender requestSender, + NonceCounter nonceCounter) { + this.config = config; + cookieDecoder = config.isUseLaxCookieEncoder() ? ClientCookieDecoder.LAX : ClientCookieDecoder.STRICT; this.channelManager = channelManager; this.requestSender = requestSender; this.nonceCounter = nonceCounter; @@ -165,6 +173,7 @@ public boolean exitAfterHandling407(Channel channel, NettyResponseFuture futu HttpHeaders requestHeaders = new DefaultHttpHeaders().add(request.getHeaders()); RequestBuilder nextRequestBuilder = future.getCurrentRequest().toBuilder().setHeaders(requestHeaders); + refreshCookies(nextRequestBuilder, request, response); if (future.getCurrentRequest().getUri().isSecured()) { nextRequestBuilder.setMethod(CONNECT); } @@ -317,6 +326,7 @@ public boolean exitAfterHandling407(Channel channel, NettyResponseFuture futu } RequestBuilder nextRequestBuilder = future.getCurrentRequest().toBuilder().setHeaders(requestHeaders); + refreshCookies(nextRequestBuilder, request, response); if (future.getCurrentRequest().getUri().isSecured()) { nextRequestBuilder.setMethod(CONNECT); } @@ -371,4 +381,11 @@ private static void ntlmProxyChallenge(String authenticateHeader, HttpHeaders re requestHeaders.set(PROXY_AUTHORIZATION, "NTLM " + challengeHeader); } } + // The challenge may have set cookies, and the retry has to carry those, not the values it went out with. + private void refreshCookies(RequestBuilder retry, Request request, HttpResponse response) { + CookieStore cookieStore = config.getCookieStore(); + if (cookieStore != null) { + CallerCookies.refresh(retry, request, response, cookieStore, cookieDecoder); + } + } } 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 2225d2687..90d1e84b1 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 @@ -48,9 +48,7 @@ import java.io.File; import java.io.IOException; import java.io.InputStream; -import java.util.ArrayList; import java.util.HashSet; -import java.util.List; import java.util.Set; import static io.netty.handler.codec.http.HttpHeaderNames.AUTHORIZATION; @@ -60,7 +58,6 @@ import static io.netty.handler.codec.http.HttpHeaderNames.HOST; import static io.netty.handler.codec.http.HttpHeaderNames.LOCATION; import static io.netty.handler.codec.http.HttpHeaderNames.PROXY_AUTHORIZATION; -import static io.netty.handler.codec.http.HttpHeaderNames.SET_COOKIE; import static org.asynchttpclient.uri.Uri.HTTP; import static org.asynchttpclient.uri.Uri.HTTPS; import static org.asynchttpclient.uri.Uri.WS; @@ -212,7 +209,7 @@ public boolean exitAfterHandlingRedirect(Channel channel, NettyResponseFuture requestBuilder.resetCookies(); } else { requestBuilder.setCookies(cookieStore == null - ? request.getCookies() : callersOwnCookies(request, response, cookieStore, newUri)); + ? request.getCookies() : CallerCookies.of(request, response, cookieStore, newUri, cookieDecoder)); } requestBuilder.setMethod(switchToGet ? GET : originalMethod) @@ -460,55 +457,6 @@ private enum BodyRepresentation { NONE } - /** - * The request's cookies minus the ones the store put there: those this response set, rotated or deleted, - * and those the store still holds with the same value. A caller's cookie sharing only a name with a stored - * one stays the caller's. - */ - private List callersOwnCookies(Request request, HttpResponse response, CookieStore cookieStore, Uri newUri) { - List cookies = request.getCookies(); - if (cookies.isEmpty()) { - return cookies; - } - // Only what the store took counts: a Set-Cookie it refused, or one for another path, leaves the - // caller's cookie of that name in place. - Set setByResponse = new HashSet<>(); - List next = null; - for (String header : response.headers().getAll(SET_COOKIE)) { - Cookie cookie = cookieDecoder.decode(header); - if (cookie == null) { - continue; - } - if (cookie.maxAge() != Cookie.UNDEFINED_MAX_AGE && cookie.maxAge() <= 0) { - setByResponse.add(cookie.name()); - continue; - } - if (next == null) { - next = cookieStore.get(newUri); - } - if (holdsSameValue(next, cookie)) { - setByResponse.add(cookie.name()); - } - } - List stored = cookieStore.get(request.getUri()); - List callers = new ArrayList<>(cookies.size()); - for (Cookie cookie : cookies) { - if (!setByResponse.contains(cookie.name()) && !holdsSameValue(stored, cookie)) { - callers.add(cookie); - } - } - return callers; - } - - private static boolean holdsSameValue(List stored, Cookie cookie) { - for (Cookie candidate : stored) { - if (candidate.name().equals(cookie.name()) && candidate.value().equals(cookie.value())) { - return true; - } - } - return false; - } - private static HttpHeaders propagatedHeaders(Request request, Realm realm, boolean keepBody, boolean stripAuthorization) { HttpHeaders headers = request.getHeaders().copy().remove(HOST); diff --git a/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Unauthorized401Interceptor.java b/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Unauthorized401Interceptor.java index 2d2b33d68..72ba3782d 100644 --- a/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Unauthorized401Interceptor.java +++ b/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Unauthorized401Interceptor.java @@ -46,6 +46,10 @@ import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.List; +import org.asynchttpclient.AsyncHttpClientConfig; +import io.netty.handler.codec.http.cookie.ClientCookieDecoder; +import org.asynchttpclient.cookie.CookieStore; +import org.asynchttpclient.RequestBuilder; import static io.netty.handler.codec.http.HttpHeaderNames.AUTHORIZATION; import static io.netty.handler.codec.http.HttpHeaderNames.WWW_AUTHENTICATE; @@ -59,11 +63,16 @@ public class Unauthorized401Interceptor { private static final Logger LOGGER = LoggerFactory.getLogger(Unauthorized401Interceptor.class); + private final AsyncHttpClientConfig config; + private final ClientCookieDecoder cookieDecoder; private final ChannelManager channelManager; private final NettyRequestSender requestSender; private final NonceCounter nonceCounter; - Unauthorized401Interceptor(ChannelManager channelManager, NettyRequestSender requestSender, NonceCounter nonceCounter) { + Unauthorized401Interceptor(AsyncHttpClientConfig config, ChannelManager channelManager, NettyRequestSender requestSender, + NonceCounter nonceCounter) { + this.config = config; + cookieDecoder = config.isUseLaxCookieEncoder() ? ClientCookieDecoder.LAX : ClientCookieDecoder.STRICT; this.channelManager = channelManager; this.requestSender = requestSender; this.nonceCounter = nonceCounter; @@ -134,7 +143,9 @@ public boolean exitAfterHandling401(Channel channel, NettyResponseFuture futu future.setChannelState(ChannelState.NEW); HttpHeaders requestHeaders = new DefaultHttpHeaders().add(request.getHeaders()); - final Request nextRequest = future.getCurrentRequest().toBuilder().setHeaders(requestHeaders).build(); + RequestBuilder retry = future.getCurrentRequest().toBuilder().setHeaders(requestHeaders); + refreshCookies(retry, request, response); + final Request nextRequest = retry.build(); if (LOGGER.isDebugEnabled()) { LOGGER.debug("Sending authentication to {}", request.getUri().toUrlWithoutUserInfo()); } @@ -280,7 +291,9 @@ public boolean exitAfterHandling401(Channel channel, NettyResponseFuture futu throw new IllegalStateException("Invalid Authentication scheme " + realm.getScheme()); } - final Request nextRequest = future.getCurrentRequest().toBuilder().setHeaders(requestHeaders).build(); + RequestBuilder retry = future.getCurrentRequest().toBuilder().setHeaders(requestHeaders); + refreshCookies(retry, request, response); + final Request nextRequest = retry.build(); if (LOGGER.isDebugEnabled()) { LOGGER.debug("Sending authentication to {}", request.getUri().toUrlWithoutUserInfo()); @@ -335,4 +348,11 @@ private static void kerberosChallenge(Realm realm, Request request, HttpHeaders realm.getLoginContextName()).generateToken(host); headers.set(AUTHORIZATION, NEGOTIATE + ' ' + challengeHeader); } + // The challenge may have set cookies, and the retry has to carry those, not the values it went out with. + private void refreshCookies(RequestBuilder retry, Request request, HttpResponse response) { + CookieStore cookieStore = config.getCookieStore(); + if (cookieStore != null) { + CallerCookies.refresh(retry, request, response, cookieStore, cookieDecoder); + } + } } diff --git a/client/src/test/java/org/asynchttpclient/netty/handler/intercept/AuthRetryCookieTest.java b/client/src/test/java/org/asynchttpclient/netty/handler/intercept/AuthRetryCookieTest.java new file mode 100644 index 000000000..273567fd6 --- /dev/null +++ b/client/src/test/java/org/asynchttpclient/netty/handler/intercept/AuthRetryCookieTest.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2026 AsyncHttpClient Project. All rights reserved. + * + * Licensed 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. + */ +package org.asynchttpclient.netty.handler.intercept; + +import io.netty.handler.codec.http.cookie.DefaultCookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.asynchttpclient.AbstractBasicTest; +import org.asynchttpclient.AsyncHttpClient; +import org.asynchttpclient.BoundRequestBuilder; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.handler.AbstractHandler; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import static org.asynchttpclient.Dsl.asyncHttpClient; +import static org.asynchttpclient.Dsl.basicAuthRealm; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * The retry after a 401 must carry the cookies the challenge set, not the ones the first attempt went out with. + */ +public class AuthRetryCookieTest extends AbstractBasicTest { + + private static final String RECEIVED_COOKIE = "received-cookie"; + + @Override + public AbstractHandler configureHandler() { + return new AbstractHandler() { + @Override + public void handle(String target, Request baseRequest, HttpServletRequest request, + HttpServletResponse response) throws IOException { + if ("/seed".equals(target)) { + response.addHeader("Set-Cookie", "SID=old; Path=/"); + } else if (request.getHeader("Authorization") == null) { + response.addHeader("Set-Cookie", "SID=new; Path=/"); + response.setHeader("WWW-Authenticate", "Basic realm=\"test\""); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + } else { + String cookie = request.getHeader("Cookie"); + if (cookie != null) { + response.setHeader(RECEIVED_COOKIE, cookie); + } + } + baseRequest.setHandled(true); + } + }; + } + + @Test + void theRetrySendsTheSessionTheChallengeSet() throws Exception { + assertEquals(new HashSet<>(Arrays.asList("SID=new")), cookiesOnRetry(null)); + } + + @Test + void theCallersCookieSurvivesTheRetry() throws Exception { + assertEquals(new HashSet<>(Arrays.asList("X=1", "SID=new")), cookiesOnRetry(new DefaultCookie("X", "1"))); + } + + private Set cookiesOnRetry(DefaultCookie callerCookie) throws Exception { + try (AsyncHttpClient client = asyncHttpClient()) { + client.prepareGet(url("/seed")).execute().get(TIMEOUT, TimeUnit.SECONDS); + BoundRequestBuilder request = client.prepareGet(url("/protected")) + .setRealm(basicAuthRealm("user", "pass").setUsePreemptiveAuth(false)); + if (callerCookie != null) { + request.addCookie(callerCookie); + } + String header = request.execute().get(TIMEOUT, TimeUnit.SECONDS).getHeader(RECEIVED_COOKIE); + return header == null ? new HashSet<>() : new HashSet<>(Arrays.asList(header.split("; "))); + } + } + + private String url(String path) { + return "http://localhost:" + port1 + path; + } +}